@abcagency/hc-ui-components 1.9.8 → 1.9.10
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 +3 -5
- package/dist/contexts/mapListContext.js.map +1 -1
- package/dist/types/util/algoliaSearchUtil.d.ts +1 -0
- package/dist/types/util/filterUtil.d.ts +2 -1
- package/dist/util/algoliaSearchUtil.js +23 -11
- package/dist/util/algoliaSearchUtil.js.map +1 -1
- package/dist/util/filterUtil.js +88 -35
- 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 +43 -44
- package/src/util/algoliaSearchUtil.js +12 -0
- package/src/util/fieldMapper.js +6 -2
- package/src/util/filterUtil.js +48 -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,50 +389,49 @@ 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,
|
|
433
431
|
listingEntities,
|
|
434
432
|
favorites,
|
|
435
|
-
siteConfig
|
|
433
|
+
siteConfig,
|
|
434
|
+
firstLoad
|
|
436
435
|
);
|
|
437
436
|
filteredListings = tempFilteredListings;
|
|
438
437
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
let algoliaClient = null;
|
|
2
2
|
let algoliaIndexName = null;
|
|
3
3
|
let algoliaSearchModule = null;
|
|
4
|
+
let algoliaInitializing = false;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Lazy load algoliasearch to avoid SSR issues
|
|
@@ -26,17 +27,21 @@ export const initializeAlgoliaSearch = async (appId, apiKey, indexName) => {
|
|
|
26
27
|
return false;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
algoliaInitializing = true;
|
|
29
31
|
try {
|
|
30
32
|
const algoliasearch = await getAlgoliaSearch();
|
|
31
33
|
if (!algoliasearch) {
|
|
32
34
|
console.warn('Algolia search not available (SSR context)');
|
|
35
|
+
algoliaInitializing = false;
|
|
33
36
|
return false;
|
|
34
37
|
}
|
|
35
38
|
algoliaClient = algoliasearch(appId, apiKey);
|
|
36
39
|
algoliaIndexName = indexName;
|
|
40
|
+
algoliaInitializing = false;
|
|
37
41
|
return true;
|
|
38
42
|
} catch (error) {
|
|
39
43
|
console.error('Failed to initialize Algolia:', error);
|
|
44
|
+
algoliaInitializing = false;
|
|
40
45
|
return false;
|
|
41
46
|
}
|
|
42
47
|
};
|
|
@@ -48,6 +53,13 @@ export const isAlgoliaAvailable = () => {
|
|
|
48
53
|
return algoliaClient !== null && algoliaIndexName !== null;
|
|
49
54
|
};
|
|
50
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check if Algolia is currently initializing
|
|
58
|
+
*/
|
|
59
|
+
export const isAlgoliaInitializing = () => {
|
|
60
|
+
return algoliaInitializing;
|
|
61
|
+
};
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* Search using Algolia and return matching listing IDs with their order
|
|
53
65
|
* @param {string} query - Search query
|
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
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
/* eslint-disable no-undef */
|
|
2
2
|
import { getDistinctItemsByProximity } from '~/util/mapUtil';
|
|
3
|
-
import { isAlgoliaAvailable, filterListingsByAlgoliaSearch } from '~/util/algoliaSearchUtil';
|
|
3
|
+
import { isAlgoliaAvailable, isAlgoliaInitializing, filterListingsByAlgoliaSearch } from '~/util/algoliaSearchUtil';
|
|
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,8 @@ 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
|
+
if (value == 1) {
|
|
52
73
|
featureCounts[featureName] += 1;
|
|
53
74
|
}
|
|
54
75
|
});
|
|
@@ -146,7 +167,8 @@ export const generateFilterOptions = (
|
|
|
146
167
|
locationFilteredListings = allListings.filter(listing => {
|
|
147
168
|
return Object.entries(selectedFilters).every(([key, value]) => {
|
|
148
169
|
if (siteConfig.locationFiltersShown.includes(key)) return true;
|
|
149
|
-
|
|
170
|
+
const fieldValue = getFieldValue(listing, key);
|
|
171
|
+
if (value && typeof value === 'object' && value[fieldValue] === true) {
|
|
150
172
|
return true;
|
|
151
173
|
}
|
|
152
174
|
return false;
|
|
@@ -242,7 +264,8 @@ export const applyFilters = async (
|
|
|
242
264
|
query,
|
|
243
265
|
listingEntities,
|
|
244
266
|
favorites,
|
|
245
|
-
siteConfig
|
|
267
|
+
siteConfig,
|
|
268
|
+
isFirstLoad = false
|
|
246
269
|
) => {
|
|
247
270
|
let results = allListings;
|
|
248
271
|
let invertedSpecialFeaturesMap;
|
|
@@ -270,16 +293,29 @@ export const applyFilters = async (
|
|
|
270
293
|
results = results.filter(listing => {
|
|
271
294
|
return Object.entries(filterItems).some(([filterName, filterValue]) => {
|
|
272
295
|
const listingFieldName = invertedSpecialFeaturesMap[filterName];
|
|
273
|
-
|
|
296
|
+
const value = getFieldValue(listing, listingFieldName);
|
|
297
|
+
return filterValue && value == 1;
|
|
274
298
|
});
|
|
275
299
|
});
|
|
276
300
|
} else if (Object.keys(filterItems).length > 0) {
|
|
277
|
-
results = results.filter(listing =>
|
|
278
|
-
|
|
279
|
-
|
|
301
|
+
results = results.filter(listing => {
|
|
302
|
+
const value = getFieldValue(listing, formattedField);
|
|
303
|
+
return Object.prototype.hasOwnProperty.call(filterItems, value);
|
|
304
|
+
});
|
|
280
305
|
}
|
|
281
306
|
}
|
|
282
307
|
if (query) {
|
|
308
|
+
// Wait for Algolia to finish initializing ONLY on first load (from URL/localStorage)
|
|
309
|
+
// Don't wait on subsequent manual searches to avoid slowing down the interface
|
|
310
|
+
if (isFirstLoad && isAlgoliaInitializing()) {
|
|
311
|
+
// Wait up to 500ms for Algolia to initialize on first load
|
|
312
|
+
let attempts = 0;
|
|
313
|
+
while (isAlgoliaInitializing() && attempts < 10) {
|
|
314
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
315
|
+
attempts++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
283
319
|
// Use Algolia if available, otherwise fall back to Fuse.js
|
|
284
320
|
if (isAlgoliaAvailable()) {
|
|
285
321
|
try {
|