openvibers/openvibe.shared offer-card.js

offer-card.js

main 2.9 KB · 45 lines · javascript

1'use strict';
2
3/**
4 * One offer from the network's shops (open.shoes, Deals, Coupons): what it is, where, the all-in price and how fresh
5 * that price is. Unknown shipping and an unknown check time stay unknown; the card never invents them. Opus's design
6 * (11-09): the title and the shop on the left, the price large on the right with what it includes under it, one quiet
7 * freshness line; styles in kit-data.css (`.ov-offer`), so a site that loads the kit gets the look.
8 *
9 * offerCard({ title, url, store, price | all_in, currency, shipping, size, condition, checked_at, stale })
10 */
11const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
12
13function money(amount, currency) {
14 const code = /^[A-Z]{3}$/.test(String(currency || '')) ? currency : 'USD';
15 try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(amount); } catch { return `${code} ${amount.toFixed(2)}`; }
16}
17
18function checkedText(iso, now = Date.now()) {
19 const t = Date.parse(String(iso || ''));
20 if (!Number.isFinite(t)) return 'Check time not known yet';
21 const mins = Math.max(0, Math.round((now - t) / 60000));
22 if (mins < 2) return 'Checked just now';
23 if (mins < 60) return `Checked ${mins} minutes ago`;
24 const hours = Math.round(mins / 60);
25 if (hours < 24) return `Checked ${hours} hour${hours === 1 ? '' : 's'} ago`;
26 return `Checked ${new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })}`;
27}
28
29function offerCard(offer = {}, { now = Date.now() } = {}) {
30 const href = typeof offer.url === 'string' && /^https?:\/\//i.test(offer.url) ? offer.url : '';
31 const allIn = Number(offer.all_in ?? offer.price);
32 const price = Number.isFinite(allIn) ? money(allIn, offer.currency) : '';
33 const shipping = offer.shipping == null || offer.shipping === '' ? 'Shipping not stated'
34 : Number(offer.shipping) === 0 ? 'Free shipping' : `+ ${money(Number(offer.shipping), offer.currency)} shipping`;
35 const title = esc(offer.title || offer.store || 'Offer');
36 const facts = [offer.store ? esc(offer.store) : 'Shop not known', offer.size ? `Size ${esc(offer.size)}` : '', offer.condition ? esc(offer.condition) : ''].filter(Boolean).join(' · ');
37 return `<article class="ov-offer${offer.stale ? ' is-stale' : ''}" data-offer-card>`
38 + `<div class="ov-offer-main"><h3 class="ov-offer-title">${href ? `<a href="${esc(href)}" rel="nofollow noopener">${title}</a>` : title}</h3><p class="ov-offer-facts">${facts}</p></div>`
39 + `<div class="ov-offer-price">${price ? `<b>${esc(price)}</b>` : '<b class="is-unknown">Price not known yet</b>'}<small>${esc(shipping)}</small></div>`
40 + `<p class="ov-offer-fresh">${esc(checkedText(offer.checked_at, now))}${offer.stale ? ' · <span class="ov-offer-stale">may have changed</span>' : ''}</p>`
41 + '</article>';
42}
43
44module.exports = { offerCard };
45