@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.
@@ -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
- 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,
@@ -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}>
@@ -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
- 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,9 @@ 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
+ // 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, countListings, fieldName, filterConfig.hideZeroResults)
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
- if (value && typeof value === 'object' && value[listing.fields[key]] === true) {
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: cleanFilters,
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
- return filterValue && listing.fields[listingFieldName] == 1;
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
- Object.prototype.hasOwnProperty.call(filterItems, listing.fields[formattedField])
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) {