PageSourceSearch

https://www.hurrcollective.com/assets/modules/plp/listing_item_tem…5ab362589c560f6337356f5e2d629563008fbd3b.js

js hurrcollective.com collected 2026-09-24 20:36:14 UTC 9,340 bytes, 199 lines download raw bytes

1// listing_item_template.js
2// A helper class to separate template rendering responsibilities from the controller
3
4export default class ListingItemTemplate {
5  constructor(options = {}) {
6    this.currentUserSlug = options.currentUserSlug || '';
7    this.userId = options.userId || null;
8    this.currentUserId = options.currentUserId || null;
9    this.collection = options.collection || '';
10    this.inIos = options.inIos || false;
11    this.editBoldSvgPath = options.editBoldSvgPath || '/assets/components/edit-bold.svg';
12    this.fxRate = options.fxRate || 1;
13    this.fxCurrencySymbol = options.fxCurrencySymbol || '£';
14    this.imageSearch = options.imageSearch || false;
15    this.plpExperiment = options.plpExperiment || false;
16    this.plpSortExperimentItemListName = options.plpSortExperimentItemListName || '';
17  }
18
19  renderItem(result) {
20    const listingPath = this._buildListingPath(result.getRaw('slug'));
21    return `
22      <div class="item-card2" id="${result.getRaw('id')}"
23           data-action="click->app-search--listing-filters#clickThroughTracking click->app-search--listing-filters#redirectWithQueryParams">
24
25        <a href="${listingPath}" class="item-link" target="_blank"
26           data-controller="google-tag-manager--enhanced-ecommerce"
27           data-google-tag-manager--enhanced-ecommerce-item-name="${result.getRaw('item_name')}"
28           data-google-tag-manager--enhanced-ecommerce-item-uid="${result.getRaw('public_uid')}"
29           data-google-tag-manager--enhanced-ecommerce-item-categories="${result.getRaw('tag_list').join(', ')}"
30           data-google-tag-manager--enhanced-ecommerce-item-brand="${result.getRaw('designer_brand')}"
31           data-google-tag-manager--enhanced-ecommerce-item-colour="${result.getRaw('colour_category')}"
32           data-google-tag-manager--enhanced-ecommerce-item-price="${result.getRaw('listing_view_price')}"
33           data-google-tag-manager--enhanced-ecommerce-item-list-name="${this.plpSortExperimentItemListName}"
34           data-action="google-tag-manager--enhanced-ecommerce#productClicks">
35
36          ${this.renderImageContainer(result)}
37
38          <div class="item-details position-relative">
39            <div class="managed-tag-wrapper">${this.renderItemBadge(result)}</div>
40            <p class="font-weight-bold text-hurr-black mb-0 text-uppercase">${result.getRaw('designer_brand')}</p>
41            <p class="mb-0 text-hurr-black pb-1">${result.getRaw('unit_sizes_available')}</p>
42            ${this.renderPricing(result)}
43          </div>
44        </a>
45      </div>
46    `;
47  }
48
49  renderImageContainer(result) {
50    return `
51      <div class="mx-auto image-wrapper position-relative" data-controller="item-card plp-wishlist"
52           data-action="mouseenter->item-card#showSecondImage mouseleave->item-card#showMainImage">
53
54        <div class="image" style="background-image: url(${result.getRaw('cl_image_url')}); opacity: 1;" data-item-card-target="firstImage"></div>
55        <div class="image" style="background-image: url(${result.getRaw('second_image_url')}); opacity: 0;" data-item-card-target="secondImage"></div>
56
57        <div class="overlay-buttons position-absolute d-flex w-100 p-2 justify-content-between">
58          ${this.renderEditButton(result)}
59          ${this.renderBuyNowBadge(result)}
60          ${this.renderWishlistButton(result)}
61
62        </div>
63      </div>
64    `;
65  }
66
67  renderEditButton(result) {
68    if (!this.currentUserId || this.currentUserId !== this.userId) {
69      return '';
70    }
71
72    const slug = result.getRaw('slug');
73    return `
74      <a href="/listings/${slug}/modals/edit" class="action-button text-decoration-none"
75         data-turbo-stream="true" data-turbo-method="GET">
76        <img src="${this.editBoldSvgPath}" alt="Edit" class="edit-icon" />
77      </a>
78    `;
79  }
80
81  renderItemBadge(result) {
82    if (result.getRaw('exclusive') === 'true') {
83      return `<span class="exclusive-tag managed-item-tag-2" data-toggle="tooltip" data-placement="top" data-html="true" data-original-title="This item is available <br> exclusively to rent on <span class='hurr-font-logo'>HURR</span>">Exclusive</span>`;
84    } else if (result.getRaw('managed_item?') === 'true') {
85      return `<span class="managed-item-tag-2" data-toggle="tooltip" data-placement="top" data-html="true" data-original-title="These items are managed<br>by our in-house team.">Managed</span>`;
86    } else if (result.getRaw('top_lender?') === 'true') {
87      return `<span class="managed-item-tag-2" data-toggle="tooltip" data-placement="top" data-html="true" data-original-title="Top Lenders are amongst our<br>best performing members.">Top Lender</span>`;
88    }
89    return '';
90  }
91
92  renderPricing(result) {
93    const rrp = parseInt(result.getRaw('rrp'), 10);
94    const rrpInCurrency = parseInt(rrp * this.fxRate);
95    if (this.collection === 'resale') {
96      let copy = `${this.renderPurchaseAvailabilityLabel(result)}<p class="mb-0 d-inline"><s>${this.fxCurrencySymbol}${rrpInCurrency.toLocaleString()}</s></p>`;
97      if (!this.isMakeAnOfferResult(result)) {
98        const purchasePrice = parseFloat(result.getRaw('lowest_unit_purchase_price'));
99        const discount = Math.round(100 - (purchasePrice * 100) / rrp);
100        if (discount >= 10 && purchasePrice < rrp) {
101          copy += `<p class="d-inline text-danger pl-2">${discount}% off</p>`;
102        }
103      }
104      return copy;
105    }
106    const fontWeight = this.collection === 'resale' ? '' : 'font-weight-bold';
107    const isResaleOnly = result.getRaw('resale_only') === 'true';
108    const rentFromHtml = isResaleOnly
109      ? ''
110      : (() => {
111          const viewPriceInCurrency = parseInt(result.getRaw('listing_view_price'), 10) * this.fxRate;
112          return `<p class="mb-0 text-hurr-black ${fontWeight}">Rent from ${this.fxCurrencySymbol}${Math.round(viewPriceInCurrency).toLocaleString()}</p>`;
113        })();
114    return `${rentFromHtml}${this.renderPurchaseAvailabilityLabel(result)}<p class="mb-0 pb-3"><s>${this.fxCurrencySymbol}${rrpInCurrency.toLocaleString()}</s></p>`;
115  }
116
117  isMakeAnOfferResult(result) {
118    return result.getRaw('purchase_availability_label') === 'MAKE AN OFFER';
119  }
120
121  renderPurchaseAvailabilityLabel(result) {
122    if (!result.getRaw('purchase_availability_label')) return '';
123    let purchase_availability_label = '';
124    if (this.availableToBuyNow(result)) {
125      const purchasePrice = parseFloat(result.getRaw('lowest_unit_purchase_price'));
126      const purchasePriceInCurrency = parseInt(purchasePrice * this.fxRate);
127
128      purchase_availability_label = 'Buy Now ' + this.fxCurrencySymbol + purchasePriceInCurrency.toLocaleString();
129    } else {
130      purchase_availability_label = 'Make an offer';
131    }
132    const fontWeight = this.collection === 'resale' ? 'font-weight-bold' : '';
133    return `<p class="mb-0 text-hurr-black ${fontWeight}">${purchase_availability_label}</p>`;
134  }
135
136  availableToBuyNow(result) {
137    return (
138      result.getRaw('available_to_buy_now') === 'true' && result.getRaw('has_available_for_buy_now_units?') === 'true'
139    );
140  }
141
142  renderWishlistButton(result) {
143    const listingId = result.getRaw('id');
144    const userSlugs = JSON.parse(result.getRaw('wishers_slugs'));
145    const wishlisted = userSlugs.includes(this.currentUserSlug);
146    const heartCount = userSlugs.length >= 2 ? userSlugs.length : '';
147    const filled =
148      'https://res.cloudinary.com/dcaptnlz3/image/asset/filled-heart-icon-a8423184c157efec16d266396a600a9f.svg';
149    const empty =
150      'https://res.cloudinary.com/dcaptnlz3/image/asset/empty-heart-icon-d56bf5bf6cbfd87ebc3a94c9c7f06461.svg';
151
152    if (!this.currentUserSlug) {
153      return `
154        <a href="#" class="heart-button ml-auto text-decoration-none" data-toggle="modal" data-target="#signup-modal"">
155          <span class="heart-counter text-black">${heartCount}</span>
156          <img src="${filled}" alt="Filled" class="heart-icon filled ${wishlisted ? '' : 'd-none'}" />
157          <img src="${empty}" alt="Empty" class="heart-icon empty ${wishlisted ? 'd-none' : ''}" />
158        </a>
159      `;
160    }
161
162    return `
163      <a href="#" class="heart-button ml-auto text-decoration-none"
164         data-listing-id="${listingId}"
165         data-action="click->plp-wishlist#toggleWishlist">
166        <span class="heart-counter text-black">${heartCount}</span>
167        <img src="${filled}" alt="Filled" class="heart-icon filled ${wishlisted ? '' : 'd-none'}" />
168        <img src="${empty}" alt="Empty" class="heart-icon empty ${wishlisted ? 'd-none' : ''}" />
169      </a>
170    `;
171  }
172
173  renderBuyNowBadge(result) {
174    if (this.currentUserId != null &&
175      this.userId != null &&
176      this.currentUserId == this.userId) {
177      return '';
178    }
179
180    const listingAvailableToBuyNow = result.getRaw('available_to_buy_now');
181    if (listingAvailableToBuyNow == 'true') {
182      return `
183        <div class="buy-now-badge">
184          <span class="buy-now-badge-text">BUY NOW</span>
185        </div>
186      `;
187    } else {
188      return '';
189    }
190  }
191
192  _buildListingPath(slug) {
193    const params = [];
194    if (this.collection === 'resale') params.push('buy_now=true');
195    if (this.imageSearch) params.push('ref=image_search');
196    if (this.plpExperiment) params.push('plp_experiment=1');
197    return params.length ? `/listings/${slug}?${params.join('&')}` : `/listings/${slug}`;
198  }
199};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.