openvibers/openvibe.shared offer-card.js
offer-card.js
| 1 | ; |
| 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 | */ |
| 11 | const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); |
| 12 | |
| 13 | function 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 | |
| 18 | function 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 | |
| 29 | function 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 | |
| 44 | module.exports = { offerCard }; |
| 45 |