diff --git a/renderer/app.js b/renderer/app.js index 0ffc338..e31b893 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -347,6 +347,12 @@ function startDateLabel (dateStr) { return `Free from ${start.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}` } +// "Dec 17" for a SALES day (a date-only string parses as UTC midnight, so print it in UTC: +// in the Americas the local date would be the day before). The year shows unless it is this year's. +function saleDateLabel (d, now) { + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC', year: d.getUTCFullYear() === now.getFullYear() ? undefined : 'numeric' }) +} + function fmtChecked (ts) { if (!ts) return '' const s = Math.floor((Date.now() - ts) / 1000) @@ -1401,7 +1407,7 @@ function renderSalesCalendar () { banner.innerHTML = `
✅
No upcoming sales found
` } - const fmtDate = d => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: d.getFullYear() === now.getFullYear() ? undefined : 'numeric' }) + const fmtDate = d => saleDateLabel(d, now) SALES.forEach(s => { const start = new Date(s.start) const end = new Date(s.end) diff --git a/tools/preview/selfcheck.mjs b/tools/preview/selfcheck.mjs index 3325e0c..80394f0 100644 --- a/tools/preview/selfcheck.mjs +++ b/tools/preview/selfcheck.mjs @@ -35,7 +35,7 @@ vm.runInContext([ between('const targetKey', '\nfunction wishlistTargetTag'), between('const nextIndex', '\n// Moves focus between'), between('const fmt$ =', '\n// CSP forbids inline onerror'), - 'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, updateAfterCheck, BASE_THEMES, SALE_THEMES }', + 'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, saleDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, updateAfterCheck, BASE_THEMES, SALE_THEMES }', ].join('\n'), context) const h = context.helpers @@ -240,6 +240,14 @@ assert.equal(h.startDateLabel('2020-01-01T00:00:00Z'), 'Free now') assert.equal(h.startDateLabel('2030-01-01T00:00:00Z').startsWith('Free from '), true) assert.equal(h.fmtChecked(0), '') +// The sales calendar prints the listed day in every timezone (UTC midnight is the day before west of Greenwich) +const tz = process.env.TZ +for (const zone of ['America/Los_Angeles', 'Pacific/Auckland', 'UTC']) { + process.env.TZ = zone + assert.equal(h.saleDateLabel(new Date('2026-12-17'), new Date('2026-10-18T12:00:00Z')), 'Dec 17', zone) + assert.equal(h.saleDateLabel(new Date('2027-01-04'), new Date('2026-10-18T12:00:00Z')), 'Jan 4, 2027', zone) +} +if (tz === undefined) delete process.env.TZ; else process.env.TZ = tz assert.equal(h.fmtChecked(Date.now()), 'updated just now') assert.match(h.fmtChecked(Date.now() - 30000), /^updated 3[01]s ago$/) // the clock may tick between the two reads assert.equal(h.fmtChecked(Date.now() - 5 * 60000), 'updated 5m ago')