@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.
@@ -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
- if (!listing.fields) return false;
393
-
394
- return Object.keys(defaultFilters).every(filterKey => {
395
- const filterValues = defaultFilters[filterKey as keyof typeof defaultFilters];
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
- useEffect(() => {
416
- const processListings = async () => {
417
- // Don't process if allListings hasn't been loaded yet
418
- if (allListings.length === 0) {
419
- console.log('processListings: Skipping - allListings is empty');
420
- return;
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
- let filteredListings: Listing[];
424
- let tempSelectedFilters = selectedFilters;
425
- let tempQuery = query;
421
+ let filteredListings: Listing[];
422
+ let tempSelectedFilters = selectedFilters;
423
+ let tempQuery = query;
426
424
 
427
- console.log('processListings: Running with query:', tempQuery, 'and', allListings.length, 'listings');
425
+ console.log('processListings: Running with query:', tempQuery, 'and', allListings.length, 'listings');
428
426
 
429
- const { mapItems, filteredListings: tempFilteredListings } = await applyFilters(
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
@@ -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 => field in item.fields);
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.fields[field];
15
+ let value = getFieldValue(item, field);
12
16
 
13
17
  return (
14
18
  <Grid.Item key={field}>
@@ -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
- if (listing.fields && listing.fields[field]) {
11
- options.add(listing.fields[field]);
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
- if (!listing.fields) return;
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
- if (listing.fields[featureKey] == 1) {
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
- if (value && typeof value === 'object' && value[listing.fields[key]] === true) {
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
- return filterValue && listing.fields[listingFieldName] == 1;
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
- Object.prototype.hasOwnProperty.call(filterItems, listing.fields[formattedField])
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 {