@abcagency/hc-ui-components 1.6.5 → 1.6.7

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.
Files changed (45) hide show
  1. package/dist/components/HireControlMap.js +43 -2
  2. package/dist/components/HireControlMap.js.map +1 -1
  3. package/dist/components/containers/accordions/filter-item-container.js +3 -1
  4. package/dist/components/containers/accordions/filter-item-container.js.map +1 -1
  5. package/dist/components/containers/filter/filter-item-container.js +29 -15
  6. package/dist/components/containers/filter/filter-item-container.js.map +1 -1
  7. package/dist/components/containers/list/item-list-container.js +1 -1
  8. package/dist/components/containers/list/item-list-container.js.map +1 -1
  9. package/dist/components/modules/accordions/filterItem.js +8 -3
  10. package/dist/components/modules/accordions/filterItem.js.map +1 -1
  11. package/dist/components/modules/filter/index.js +123 -19
  12. package/dist/components/modules/filter/index.js.map +1 -1
  13. package/dist/components/modules/filter/item.js +8 -3
  14. package/dist/components/modules/filter/item.js.map +1 -1
  15. package/dist/components/modules/filter/search.js +6 -1
  16. package/dist/components/modules/filter/search.js.map +1 -1
  17. package/dist/components/modules/list/list-item/list-item.js.map +1 -1
  18. package/dist/components/modules/maps/tabs.js +1 -1
  19. package/dist/components/modules/maps/tabs.js.map +1 -1
  20. package/dist/contexts/mapListContext.js +48 -3
  21. package/dist/contexts/mapListContext.js.map +1 -1
  22. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +51 -1
  23. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  24. package/dist/styles/index.css +1 -1
  25. package/dist/types/util/filterUtil.d.ts +9 -3
  26. package/dist/util/algoliaSearchUtil.js.map +1 -1
  27. package/dist/util/filterUtil.js +77 -14
  28. package/dist/util/filterUtil.js.map +1 -1
  29. package/dist/util/twMerge.js +9 -0
  30. package/dist/util/twMerge.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/components/HireControlMap.js +48 -2
  33. package/src/components/containers/accordions/filter-item-container.js +1 -1
  34. package/src/components/containers/filter/filter-item-container.js +27 -14
  35. package/src/components/containers/list/item-list-container.tsx +1 -1
  36. package/src/components/modules/accordions/filterItem.js +16 -3
  37. package/src/components/modules/filter/index.js +161 -43
  38. package/src/components/modules/filter/item.js +15 -3
  39. package/src/components/modules/filter/search.js +7 -1
  40. package/src/components/modules/list/list-item/list-item.jsx +1 -1
  41. package/src/components/modules/maps/tabs.js +10 -10
  42. package/src/contexts/mapListContext.tsx +103 -10
  43. package/src/util/algoliaSearchUtil.js +7 -7
  44. package/src/util/filterUtil.js +63 -17
  45. package/src/util/twMerge.js +6 -0
@@ -55,6 +55,28 @@ interface MapListContextProps {
55
55
  containerStyle?: any;
56
56
  ExpandListComponent?: React.ComponentType<{ listing: any }> | ((listing: any) => JSX.Element) | null;
57
57
  noEntities?: boolean;
58
+ filterConfig?: {
59
+ hideZeroResults?: boolean;
60
+ dynamicCounts?: boolean;
61
+ showFavorites?: boolean;
62
+ collapsedByDefault?: boolean;
63
+ sortAlphabetically?: boolean;
64
+ classNames?: {
65
+ filterContainer?: string;
66
+ filterContent?: string;
67
+ filterAccordion?: string;
68
+ filterAccordionHeader?: string;
69
+ filterAccordionContent?: string;
70
+ filterItem?: string;
71
+ filterItemLabel?: string;
72
+ filterItemCheckbox?: string;
73
+ filterItemCount?: string;
74
+ searchInput?: string;
75
+ resetButton?: string;
76
+ showJobsButton?: string;
77
+ filterFooter?: string;
78
+ };
79
+ };
58
80
  }
59
81
 
60
82
  const MapListContext = createContext<MapListContextProps | undefined>(undefined);
@@ -101,6 +123,28 @@ interface MapListProviderProps {
101
123
  hideMap?: boolean;
102
124
  hideFilters?: boolean;
103
125
  noEntities?: boolean;
126
+ filterConfig?: {
127
+ hideZeroResults?: boolean;
128
+ dynamicCounts?: boolean;
129
+ showFavorites?: boolean;
130
+ collapsedByDefault?: boolean;
131
+ sortAlphabetically?: boolean;
132
+ classNames?: {
133
+ filterContainer?: string;
134
+ filterContent?: string;
135
+ filterAccordion?: string;
136
+ filterAccordionHeader?: string;
137
+ filterAccordionContent?: string;
138
+ filterItem?: string;
139
+ filterItemLabel?: string;
140
+ filterItemCheckbox?: string;
141
+ filterItemCount?: string;
142
+ searchInput?: string;
143
+ resetButton?: string;
144
+ showJobsButton?: string;
145
+ filterFooter?: string;
146
+ };
147
+ };
104
148
  }
105
149
 
106
150
  export const MapListProvider: React.FC<MapListProviderProps> = ({
@@ -128,17 +172,39 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
128
172
  localStorageKey,
129
173
  hideMap = false,
130
174
  hideFilters = false,
131
- noEntities = false
175
+ noEntities = false,
176
+ filterConfig = {
177
+ hideZeroResults: false,
178
+ dynamicCounts: true,
179
+ showFavorites: true,
180
+ collapsedByDefault: false,
181
+ sortAlphabetically: false,
182
+ classNames: {
183
+ filterContainer: '',
184
+ filterContent: '',
185
+ filterAccordion: '',
186
+ filterAccordionHeader: '',
187
+ filterAccordionContent: '',
188
+ filterItem: '',
189
+ filterItemLabel: '',
190
+ filterItemCheckbox: '',
191
+ filterItemCount: '',
192
+ searchInput: '',
193
+ resetButton: '',
194
+ showJobsButton: '',
195
+ filterFooter: ''
196
+ }
197
+ }
132
198
  }) => {
133
199
  const firstLoadFilters = () =>{
134
200
  let urlData = filtersFromURL(window.location);
135
201
  let urlFilters = urlData?.filters;
136
-
202
+
137
203
  // If ls-ignore=true is in URL, don't load from localStorage
138
204
  if (urlData?.lsIgnore) {
139
205
  return urlFilters || {};
140
206
  }
141
-
207
+
142
208
  return (setFiltersUrl === true && urlFilters && Object.keys(urlFilters).length > 0) ? urlFilters : getStorageObject(localStorageKey + 'selectedFilters', {}) || {}
143
209
  }
144
210
 
@@ -146,12 +212,12 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
146
212
  if (resetFilters) return null;
147
213
  // Check URL first
148
214
  const urlData = filtersFromURL(window.location);
149
-
215
+
150
216
  // If ls-ignore=true is in URL, don't load from localStorage
151
217
  if (urlData?.lsIgnore) {
152
218
  return urlData?.query || null;
153
219
  }
154
-
220
+
155
221
  if (setFiltersUrl === true && urlData?.query) {
156
222
  return urlData.query;
157
223
  }
@@ -277,7 +343,7 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
277
343
  console.log('processListings: Skipping - allListings is empty');
278
344
  return;
279
345
  }
280
-
346
+
281
347
  let filteredListings: Listing[];
282
348
  let tempSelectedFilters = selectedFilters;
283
349
  let tempQuery = query;
@@ -297,11 +363,11 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
297
363
  if (filterByFavorites) {
298
364
  filteredListings = filteredListings.filter((x: Listing) => favorites.includes(x.id));
299
365
  }
300
-
366
+
301
367
  // Batch state updates together
302
368
  setNewFilteredListings(filteredListings);
303
369
  setMapItems(mapItems);
304
-
370
+
305
371
  if (firstLoad && tempSelectedFilters) {
306
372
  // Update URL with filters if needed
307
373
  } else if (Object.keys(tempSelectedFilters).length === 0 && !firstLoad) {
@@ -318,6 +384,31 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
318
384
  if (tempSelectedFilters) {
319
385
  const keys = Object.keys(tempSelectedFilters);
320
386
  const lastKey = keys[keys.length - 1];
387
+
388
+ // Ensure all filterConfig properties have values
389
+ const normalizedFilterConfig = {
390
+ hideZeroResults: filterConfig?.hideZeroResults ?? false,
391
+ dynamicCounts: filterConfig?.dynamicCounts ?? true,
392
+ showFavorites: filterConfig?.showFavorites ?? true,
393
+ collapsedByDefault: filterConfig?.collapsedByDefault ?? false,
394
+ sortAlphabetically: filterConfig?.sortAlphabetically ?? false,
395
+ classNames: {
396
+ filterContainer: filterConfig?.classNames?.filterContainer ?? '',
397
+ filterContent: filterConfig?.classNames?.filterContent ?? '',
398
+ filterAccordion: filterConfig?.classNames?.filterAccordion ?? '',
399
+ filterAccordionHeader: filterConfig?.classNames?.filterAccordionHeader ?? '',
400
+ filterAccordionContent: filterConfig?.classNames?.filterAccordionContent ?? '',
401
+ filterItem: filterConfig?.classNames?.filterItem ?? '',
402
+ filterItemLabel: filterConfig?.classNames?.filterItemLabel ?? '',
403
+ filterItemCheckbox: filterConfig?.classNames?.filterItemCheckbox ?? '',
404
+ filterItemCount: filterConfig?.classNames?.filterItemCount ?? '',
405
+ searchInput: filterConfig?.classNames?.searchInput ?? '',
406
+ resetButton: filterConfig?.classNames?.resetButton ?? '',
407
+ showJobsButton: filterConfig?.classNames?.showJobsButton ?? '',
408
+ filterFooter: filterConfig?.classNames?.filterFooter ?? ''
409
+ }
410
+ };
411
+
321
412
  const options = generateFilterOptions(
322
413
  filteredListings,
323
414
  allListings,
@@ -325,7 +416,8 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
325
416
  filterOptions,
326
417
  lastKey,
327
418
  favorites,
328
- tempSelectedFilters
419
+ tempSelectedFilters,
420
+ normalizedFilterConfig
329
421
  );
330
422
  if (options) {
331
423
  setFilterOptions(options);
@@ -403,7 +495,8 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
403
495
  hiddenFilters,
404
496
  containerStyle,
405
497
  ExpandListComponent,
406
- noEntities
498
+ noEntities,
499
+ filterConfig
407
500
  }}>
408
501
  {children}
409
502
  </MapListContext.Provider>
@@ -56,11 +56,11 @@ export const searchAlgolia = async (query) => {
56
56
  // Extract IDs from the results and maintain order
57
57
  const hits = results[0]?.hits || [];
58
58
  console.log('Algolia search returned', hits.length, 'hits');
59
-
59
+
60
60
  const referenceNumbers = [];
61
61
  const ids = [];
62
62
  const orderMap = new Map(); // Map to store the Algolia result order
63
-
63
+
64
64
  hits.forEach((hit, index) => {
65
65
  if (hit.referenceNumber) {
66
66
  const refNum = parseInt(hit.referenceNumber);
@@ -92,18 +92,18 @@ export const filterListingsByAlgoliaSearch = async (listings, query) => {
92
92
 
93
93
  try {
94
94
  const { referenceNumbers, ids, orderMap } = await searchAlgolia(query);
95
-
95
+
96
96
  console.log('Filtering', listings.length, 'listings against', referenceNumbers.length, 'referenceNumbers and', ids.length, 'ids');
97
97
  console.log('Sample listing referenceNumbers:', listings.slice(0, 5).map(l => l.referenceNumber));
98
98
  console.log('Sample listing ids:', listings.slice(0, 5).map(l => l.id));
99
99
  console.log('Sample Algolia referenceNumbers:', referenceNumbers.slice(0, 5));
100
100
  console.log('Sample Algolia ids:', ids.slice(0, 5));
101
-
101
+
102
102
  // Filter listings by matching referenceNumber OR id
103
- const filtered = listings.filter(listing =>
103
+ const filtered = listings.filter(listing =>
104
104
  referenceNumbers.includes(listing.referenceNumber) || ids.includes(listing.id)
105
105
  );
106
-
106
+
107
107
  // Sort filtered listings by Algolia result order
108
108
  filtered.sort((a, b) => {
109
109
  // Get the Algolia order index for each listing
@@ -111,7 +111,7 @@ export const filterListingsByAlgoliaSearch = async (listings, query) => {
111
111
  const orderB = orderMap.get(`ref-${b.referenceNumber}`) ?? orderMap.get(`id-${b.id}`) ?? Infinity;
112
112
  return orderA - orderB;
113
113
  });
114
-
114
+
115
115
  console.log('Filtered result:', filtered.length, 'listings matched and sorted by Algolia order');
116
116
  return filtered;
117
117
  } catch (error) {
@@ -4,10 +4,10 @@ import { isAlgoliaAvailable, filterListingsByAlgoliaSearch } from '~/util/algoli
4
4
 
5
5
  import Fuse from 'fuse.js';
6
6
 
7
- export const getFilterOptions = (listings, filteredListings, field, excludeZeroCount = null) => {
7
+ export const getFilterOptions = (listings, filteredListings, field, excludeZeroCount = false, sortAlphabetically = false) => {
8
8
  const options = new Set();
9
9
  listings.forEach(listing => {
10
- if (listing.fields[field]) {
10
+ if (listing.fields && listing.fields[field]) {
11
11
  options.add(listing.fields[field]);
12
12
  }
13
13
  });
@@ -18,22 +18,32 @@ export const getFilterOptions = (listings, filteredListings, field, excludeZeroC
18
18
  });
19
19
 
20
20
  filteredListings.forEach(listing => {
21
+ if (!listing.fields) return;
21
22
  const value = listing.fields[field];
22
- if (value && optionCounts.hasOwnProperty(value)) {
23
+ if (value && Object.prototype.hasOwnProperty.call(optionCounts, value)) {
23
24
  optionCounts[value] += 1;
24
25
  }
25
26
  });
26
27
 
27
- return Array.from(options)
28
- .sort()
28
+ let result = Array.from(options)
29
+ .filter(option => option != null && option !== '') // Filter out null/empty values
29
30
  .map(option => ({
30
31
  name: option,
31
32
  count: optionCounts[option] || 0
32
33
  }))
33
34
  .filter(option => !(excludeZeroCount === true && option.count === 0));
35
+
36
+ // Sort alphabetically or by count
37
+ if (sortAlphabetically) {
38
+ result.sort((a, b) => a.name.localeCompare(b.name));
39
+ } else {
40
+ result.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
41
+ }
42
+
43
+ return result;
34
44
  };
35
45
 
36
- export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig, favorites) => {
46
+ export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig, favorites, filterConfig = {}) => {
37
47
  const specialFeatures = siteConfig.specialFeatures;
38
48
  const featureCounts = Object.keys(specialFeatures).sort().reduce((acc, key) => {
39
49
  acc[specialFeatures[key]] = 0;
@@ -48,17 +58,28 @@ export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig,
48
58
  });
49
59
  });
50
60
 
51
- const specialFeatureOptions = Object.entries(featureCounts).map(([name, count]) => ({
61
+ let specialFeatureOptions = Object.entries(featureCounts).map(([name, count]) => ({
52
62
  name,
53
63
  count
54
64
  }));
55
65
 
66
+ // Update favorite count
56
67
  for (let option of specialFeatureOptions) {
57
68
  if (option.name === 'Favorite') {
58
69
  option.count = filteredListings.filter(x => favorites.includes(x.id)).length;
59
70
  }
60
71
  }
61
72
 
73
+ // Hide favorites if filterConfig.showFavorites is false
74
+ if (filterConfig.showFavorites === false) {
75
+ specialFeatureOptions = specialFeatureOptions.filter(option => option.name !== 'Favorite');
76
+ }
77
+
78
+ // Hide zero results if filterConfig.hideZeroResults is true
79
+ if (filterConfig.hideZeroResults === true) {
80
+ specialFeatureOptions = specialFeatureOptions.filter(option => option.count > 0);
81
+ }
82
+
62
83
  return specialFeatureOptions;
63
84
  };
64
85
 
@@ -76,9 +97,19 @@ export const generateFilterOptions = (
76
97
  filterOptions,
77
98
  parentField,
78
99
  favorites,
79
- selectedFilters
100
+ selectedFilters,
101
+ filterConfig = {
102
+ hideZeroResults: false,
103
+ dynamicCounts: true,
104
+ showFavorites: true,
105
+ collapsedByDefault: false,
106
+ sortAlphabetically: false
107
+ }
80
108
  ) => {
81
109
  if (allListings.length > 0) {
110
+ // Determine which listings to use for counts based on dynamicCounts setting
111
+ const countListings = filterConfig.dynamicCounts ? filteredListings : allListings;
112
+
82
113
  const dynamicFilters = siteConfig.fieldFiltersShown.map(fieldName => {
83
114
  if (fieldName === parentField && filterOptions?.filters) {
84
115
  return filterOptions.filters.find(filter => filter.id === fieldName);
@@ -87,31 +118,31 @@ export const generateFilterOptions = (
87
118
  return {
88
119
  id: fieldName,
89
120
  title: siteConfig.fieldNames[fieldName],
90
- items: getFilterOptions(allListings, allListings, fieldName)
121
+ items: getFilterOptions(allListings, countListings, fieldName, filterConfig.hideZeroResults, filterConfig.sortAlphabetically)
91
122
  };
92
123
  }
93
124
  if(fieldName == 'subCategory' && selectedFilters.category){
94
125
  const categoryKeys = Object.keys(selectedFilters.category);
95
- const filteredListings = allListings.filter(
126
+ const filteredByCategory = allListings.filter(
96
127
  x => categoryKeys.includes(x.fields?.category)
97
128
  );
98
129
  return {
99
130
  id: fieldName,
100
131
  title: siteConfig.fieldNames[fieldName],
101
- items: getFilterOptions(allListings, filteredListings, fieldName, false)
132
+ items: getFilterOptions(allListings, filteredByCategory, fieldName, filterConfig.hideZeroResults, filterConfig.sortAlphabetically)
102
133
  };
103
134
  }
104
135
  if (fieldName == "specialFeatures") {
105
136
  return {
106
137
  id: fieldName,
107
138
  title: siteConfig.fieldNames[fieldName],
108
- items: getSpecialFeatureOptions(allListings, filteredListings, siteConfig, favorites).sort()
139
+ items: getSpecialFeatureOptions(allListings, countListings, siteConfig, favorites, filterConfig).sort()
109
140
  };
110
141
  }
111
142
  return {
112
143
  id: fieldName,
113
144
  title: siteConfig.fieldNames[fieldName],
114
- items: getFilterOptions(allListings, filteredListings, fieldName)
145
+ items: getFilterOptions(allListings, countListings, fieldName, filterConfig.hideZeroResults, filterConfig.sortAlphabetically)
115
146
  };
116
147
  });
117
148
 
@@ -186,9 +217,24 @@ export const generateFilterOptions = (
186
217
  )
187
218
  };
188
219
 
220
+ // Filter out null filters and ensure items arrays don't contain undefined values
221
+ const cleanFilters = dynamicFilters
222
+ .filter(f => f != null && f.items != null)
223
+ .map(f => ({
224
+ ...f,
225
+ items: f.items.filter(item => item != null && item.name != null)
226
+ }));
227
+
228
+ const cleanLocations = locations
229
+ .filter(l => l != null && l.items != null)
230
+ .map(l => ({
231
+ ...l,
232
+ items: l.items.filter(item => item != null && item.name != null)
233
+ }));
234
+
189
235
  return {
190
- filters: dynamicFilters,
191
- locations: locations,
236
+ filters: cleanFilters,
237
+ locations: cleanLocations,
192
238
  pointsOfInterest: pointsOfInterest
193
239
  };
194
240
  }
@@ -235,7 +281,7 @@ export const applyFilters = async (
235
281
  });
236
282
  } else if (Object.keys(filterItems).length > 0) {
237
283
  results = results.filter(listing =>
238
- filterItems.hasOwnProperty(listing.fields[formattedField])
284
+ Object.prototype.hasOwnProperty.call(filterItems, listing.fields[formattedField])
239
285
  );
240
286
  }
241
287
  }
@@ -314,7 +360,7 @@ export const filterListingsByLocation = (
314
360
  let results = allListings;
315
361
  if (selectedLocation !== null) {
316
362
  results = results.filter(item =>
317
- selectedLocation.items.hasOwnProperty(item.id)
363
+ Object.prototype.hasOwnProperty.call(selectedLocation.items, item.id)
318
364
  );
319
365
  }
320
366
  const mapItems = getDistinctItemsByProximity(results, listingEntities);
@@ -0,0 +1,6 @@
1
+ import { extendTailwindMerge } from 'tailwind-merge';
2
+
3
+ // Create a custom tailwind-merge that understands the hc- prefix
4
+ export const twMerge = extendTailwindMerge({
5
+ prefix: 'hc-'
6
+ });