@braudypedrosa/bp-listings 1.0.7 → 1.1.0

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Airbnb-style listings + map widget built with vanilla JavaScript and CSS.
4
4
 
5
- Current version: **1.0.7**
5
+ Current version: **1.1.0**
6
6
 
7
7
  ## Overview
8
8
 
@@ -115,8 +115,8 @@ When you integrate external search through `renderSearchSlot`, the recommended c
115
115
  Important:
116
116
 
117
117
  - this is a docs/demo convention only
118
- - `bp-listings` does not parse or enforce `searchData`
119
- - matching remains consumer-owned
118
+ - `bp-listings` does not parse or enforce `searchData` automatically
119
+ - matching remains consumer-owned, but helper exports are available for the documented convention
120
120
 
121
121
  ## Config Options
122
122
 
@@ -158,7 +158,6 @@ This pattern requires a bundler or dev server that can resolve npm packages and
158
158
  ```js
159
159
  import '@braudypedrosa/bp-listings';
160
160
  import '@braudypedrosa/bp-listings/styles';
161
- import '@braudypedrosa/bp-calendar/styles';
162
161
  import '@braudypedrosa/bp-search-widget/styles';
163
162
  import { BPSearchWidget } from '@braudypedrosa/bp-search-widget';
164
163
 
@@ -241,103 +240,10 @@ const filterDefinitions = [
241
240
  },
242
241
  ];
243
242
 
244
- const fieldTypeMap = new Map(
245
- [...fieldDefinitions, ...filterDefinitions].map((field) => [field.key, field.type])
246
- );
247
-
248
- const normalizeString = (value) => {
249
- if (value === null || value === undefined) return '';
250
- return String(value).trim().toLowerCase();
251
- };
252
-
253
- const toArray = (value) => {
254
- if (Array.isArray(value)) return value;
255
- if (value === null || value === undefined) return [];
256
- return [value];
257
- };
258
-
259
- const normalizeStringArray = (value) =>
260
- toArray(value).map((entry) => normalizeString(entry)).filter(Boolean);
261
-
262
- const matchesSubstring = (haystackValue, needleValue) => {
263
- const needle = normalizeString(needleValue);
264
- if (!needle) return true;
265
- return normalizeStringArray(haystackValue).some((value) => value.includes(needle));
266
- };
267
-
268
- const matchesExact = (haystackValue, needleValue) => {
269
- const needle = normalizeString(needleValue);
270
- if (!needle) return true;
271
- return normalizeStringArray(haystackValue).some((value) => value === needle);
272
- };
273
-
274
- const matchesAllChoices = (haystackValue, needleValues) => {
275
- const selectedValues = normalizeStringArray(needleValues);
276
- if (selectedValues.length === 0) return true;
277
- const availableValues = new Set(normalizeStringArray(haystackValue));
278
- return selectedValues.every((value) => availableValues.has(value));
279
- };
280
-
281
- const matchesCounter = (haystackValue, needleValue) => {
282
- const numericNeedle = Number(needleValue);
283
- const numericHaystack = Number(haystackValue);
284
- if (!Number.isFinite(numericNeedle)) return true;
285
- if (!Number.isFinite(numericHaystack)) return false;
286
- return numericHaystack >= numericNeedle;
287
- };
288
-
289
- const isValidDateString = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
290
-
291
- const matchesAvailability = (availability, checkIn, checkOut) => {
292
- if (!Array.isArray(availability) || !isValidDateString(checkIn) || !isValidDateString(checkOut)) {
293
- return false;
294
- }
295
-
296
- return availability.some((windowRange) => (
297
- windowRange &&
298
- isValidDateString(windowRange.start) &&
299
- isValidDateString(windowRange.end) &&
300
- windowRange.start <= checkIn &&
301
- windowRange.end >= checkOut
302
- ));
303
- };
304
-
305
- const matchValues = (type, listingValue, submittedValue) => {
306
- if (type === 'checkbox') return matchesAllChoices(listingValue, submittedValue);
307
- if (type === 'select' || type === 'radio') return matchesExact(listingValue, submittedValue);
308
- if (type === 'counter') return matchesCounter(listingValue, submittedValue);
309
- return matchesSubstring(listingValue, submittedValue);
310
- };
311
-
312
- const matchListing = (listing, payload) => {
313
- const searchData = listing.searchData || {};
314
- const fieldValues = searchData.fields || {};
315
- const filterValues = searchData.filters || {};
316
-
317
- if (!matchesSubstring(searchData.location, payload.location)) {
318
- return false;
319
- }
320
-
321
- if (payload.checkIn && payload.checkOut) {
322
- if (!matchesAvailability(searchData.availability, payload.checkIn, payload.checkOut)) {
323
- return false;
324
- }
325
- }
326
-
327
- for (const [key, value] of Object.entries(payload.customFields || {})) {
328
- if (!matchValues(fieldTypeMap.get(key), fieldValues[key], value)) {
329
- return false;
330
- }
331
- }
332
-
333
- for (const [key, value] of Object.entries(payload.filters || {})) {
334
- if (!matchValues(fieldTypeMap.get(key), filterValues[key], value)) {
335
- return false;
336
- }
337
- }
338
-
339
- return true;
340
- };
243
+ const matchListing = ListingsMap.createSearchDataMatcher({
244
+ fields: fieldDefinitions,
245
+ filters: filterDefinitions,
246
+ });
341
247
 
342
248
  const listingsWidget = ListingsMap.init({
343
249
  container: '#widget',
@@ -352,7 +258,10 @@ const listingsWidget = ListingsMap.init({
352
258
  datepickerPlacement: 'auto',
353
259
  },
354
260
  onSearch: (payload) => {
355
- const filteredListings = allListings.filter((listing) => matchListing(listing, payload));
261
+ const filteredListings = ListingsMap.filterListingsBySearchData(allListings, payload, {
262
+ fields: fieldDefinitions,
263
+ filters: filterDefinitions,
264
+ });
356
265
  widget.setListings(filteredListings);
357
266
  },
358
267
  });
@@ -378,6 +287,11 @@ Returned widget instance exposes:
378
287
  - `goToPage(pageNumber)`
379
288
  - `destroy()`
380
289
 
290
+ Helper exports on `window.ListingsMap` or the package module:
291
+
292
+ - `createSearchDataMatcher({ fields, filters })`
293
+ - `filterListingsBySearchData(listings, payload, { fields, filters })`
294
+
381
295
  ## Styling and Theming
382
296
 
383
297
  All classes are scoped with `.lm-*`.
@@ -407,7 +321,7 @@ npm run dev
407
321
  ```
408
322
 
409
323
  The local demo uses Vite only for development so it can resolve `@braudypedrosa/bp-search-widget`,
410
- `@braudypedrosa/bp-calendar`, and their styles. The published `bp-listings` package format is unchanged.
324
+ `@braudypedrosa/bp-ui-components`, and their styles. The published `bp-listings` package format is unchanged.
411
325
 
412
326
  ## Notes
413
327