openvibers/openvibe.coupons Commits
Store page: product deals twenty at a time (#159)
Changes
@@@ -293,6 +293,7 @@ function createPublicRoutes(ctx) {m, domains, hints: await coupons.merchantHints(m.id), codes, deals, ended, reportsThisWeek, price,now, windowDays: confidence.WINDOW_DAYS,viewer: req.viewer, csrf: csrf(req), back: `/m/${m.slug}${tab === 'codes' ? '' : `?tab=${tab}`}`, flashMsg, tab, success,++ dealsShown: Math.min(500, Math.max(0, Math.floor(Number(req.query.deals)) || 0)),watching: await watches.isWatching(req.viewer.subject, m),}),}, { cacheable: !flashMsg });@@@ -210,7 +210,10 @@ ${facts}</section>` : h`<section class="ovk-section"><h2>Quick facts</h2><p clas* The store page. codes: the shop's active codes (a code for the whole shop), deals: the codes for one product,* ended: the codes that ended, reportsThisWeek: how many reports the shop's codes collected this week.*/--function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, price, now, windowDays, viewer, csrf, watching, back, flashMsg, tab = 'codes', success = null }) {++// Product deals show a page at a time (2026-10-31: Amazon's 200 made its store page 62 phone screens long).++const DEALS_PAGE = 20;++++function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, price, now, windowDays, viewer, csrf, watching, back, flashMsg, tab = 'codes', success = null, dealsShown = DEALS_PAGE }) {const base = `/m/${m.slug}`;const host = (domains[0] && domains[0].host) || (m.homepage_url ? hostOf(m.homepage_url) : m.name);const all = [...codes, ...deals];@@@ -239,12 +242,14 @@ function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, prconst flashMsgHtml = flashMsg ? h`<p class="notice" role="status">${flashMsg}</p>` : '';++ const shown = Math.max(DEALS_PAGE, Math.min(deals.length, Math.floor(Number(dealsShown)) || DEALS_PAGE));let body;if (tab === 'about') {body = h`${aboutTab({ m, domains, hints, codes: all, price, now, windowDays })}`;} else if (tab === 'deals') {body = h`<p class="ovk-note cpn-lede">Codes that take money off one product, as the source posted them.</p>--${deals.length ? h`${deals.map((v) => couponCard(v, { viewer, csrf, back, link: true, shopUrl: m.homepage_url }))}` : h`<p class="empty">No product deals for ${m.name} right now.</p>`}++${deals.length ? h`${deals.slice(0, shown).map((v) => couponCard(v, { viewer, csrf, back, link: true, shopUrl: m.homepage_url }))}++${shown < deals.length ? h`<p class="cpn-more"><a class="ovk-btn ovk-btn--quiet" href="${base}?tab=deals&deals=${String(Math.min(deals.length, shown + DEALS_PAGE))}">Show ${String(Math.min(DEALS_PAGE, deals.length - shown))} more <span class="cpn-muted">(${String(shown)} of ${String(deals.length)})</span></a></p>` : ''}` : h`<p class="empty">No product deals for ${m.name} right now.</p>`}${merchantTrend(success, m.name)}`;} else if (!codes.length) {// A shop with no codes says so, and offers the watch that tells us when one appears.@@@ -148,6 +148,25 @@ function bestCardCode(html) {assert.doesNotMatch(about.text, /verified/i);});++ await check('product deals show twenty at a time, with a link to the next ones (Amazon had 200 on one page)', async () => {++ const shop = await t.merchant({ name: 'Deals Shop', host: 'deals-shop.com' });++ const people = Array.from({ length: 5 }, (_, i) => t.network.addUser('dealer-' + i));++ let n = 0;++ while (n < 25) {++ await submitCode(t, people[n % 5], shop.slug, 'DEAL' + String(n).padStart(3, '0'), 'Widget ' + n + ' for $' + (10 + n));++ n++;++ }++ const first = await t.get('/m/' + shop.slug + '?tab=deals');++ assert.strictEqual(first.status, 200);++ assert.strictEqual((first.text.match(/One product/g) || []).length, 20, 'the first page shows twenty');++ assert.match(first.text, /href="\/m\/[^"]+\?tab=deals&deals=25">Show 5 more/, 'and a link to the rest');++ const all = await t.get('/m/' + shop.slug + '?tab=deals&deals=25');++ assert.strictEqual((all.text.match(/One product/g) || []).length, 25, 'the link shows them all');++ assert.doesNotMatch(all.text, /Show \d+ more/, 'with nothing left to show, no link');++ const silly = await t.get('/m/' + shop.slug + '?tab=deals&deals=-4');++ assert.strictEqual((silly.text.match(/One product/g) || []).length, 20, 'a nonsense count falls back to a page');++ });++await t.close();done();})();
@@ -293,6 +293,7 @@ function createPublicRoutes(ctx) {
@@ -293,6 +293,7 @@ function createPublicRoutes(ctx) {
m, domains, hints: await coupons.merchantHints(m.id), codes, deals, ended, reportsThisWeek, price,
m, domains, hints: await coupons.merchantHints(m.id), codes, deals, ended, reportsThisWeek, price,
now, windowDays: confidence.WINDOW_DAYS,
now, windowDays: confidence.WINDOW_DAYS,
viewer: req.viewer, csrf: csrf(req), back: `/m/${m.slug}${tab === 'codes' ? '' : `?tab=${tab}`}`, flashMsg, tab, success,
viewer: req.viewer, csrf: csrf(req), back: `/m/${m.slug}${tab === 'codes' ? '' : `?tab=${tab}`}`, flashMsg, tab, success,
+ dealsShown: Math.min(500, Math.max(0, Math.floor(Number(req.query.deals)) || 0)),
watching: await watches.isWatching(req.viewer.subject, m),
watching: await watches.isWatching(req.viewer.subject, m),
}),
}),
}, { cacheable: !flashMsg });
}, { cacheable: !flashMsg });
@@ -210,7 +210,10 @@ ${facts}</section>` : h`<section class="ovk-section"><h2>Quick facts</h2><p clas
@@ -210,7 +210,10 @@ ${facts}</section>` : h`<section class="ovk-section"><h2>Quick facts</h2><p clas
* The store page. codes: the shop's active codes (a code for the whole shop), deals: the codes for one product,
* The store page. codes: the shop's active codes (a code for the whole shop), deals: the codes for one product,
* ended: the codes that ended, reportsThisWeek: how many reports the shop's codes collected this week.
* ended: the codes that ended, reportsThisWeek: how many reports the shop's codes collected this week.
*/
*/
-function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, price, now, windowDays, viewer, csrf, watching, back, flashMsg, tab = 'codes', success = null }) {
+// Product deals show a page at a time (2026-10-31: Amazon's 200 made its store page 62 phone screens long).
+const DEALS_PAGE = 20;
+
+function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, price, now, windowDays, viewer, csrf, watching, back, flashMsg, tab = 'codes', success = null, dealsShown = DEALS_PAGE }) {
const base = `/m/${m.slug}`;
const base = `/m/${m.slug}`;
const host = (domains[0] && domains[0].host) || (m.homepage_url ? hostOf(m.homepage_url) : m.name);
const host = (domains[0] && domains[0].host) || (m.homepage_url ? hostOf(m.homepage_url) : m.name);
const all = [...codes, ...deals];
const all = [...codes, ...deals];
@@ -239,12 +242,14 @@ function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, pr
@@ -239,12 +242,14 @@ function storePage({ m, domains, hints, codes, deals, ended, reportsThisWeek, pr
const flashMsgHtml = flashMsg ? h`<p class="notice" role="status">${flashMsg}</p>` : '';
const flashMsgHtml = flashMsg ? h`<p class="notice" role="status">${flashMsg}</p>` : '';
+ const shown = Math.max(DEALS_PAGE, Math.min(deals.length, Math.floor(Number(dealsShown)) || DEALS_PAGE));
let body;
let body;
if (tab === 'about') {
if (tab === 'about') {
body = h`${aboutTab({ m, domains, hints, codes: all, price, now, windowDays })}`;
body = h`${aboutTab({ m, domains, hints, codes: all, price, now, windowDays })}`;
} else if (tab === 'deals') {
} else if (tab === 'deals') {
body = h`<p class="ovk-note cpn-lede">Codes that take money off one product, as the source posted them.</p>
body = h`<p class="ovk-note cpn-lede">Codes that take money off one product, as the source posted them.</p>
-${deals.length ? h`${deals.map((v) => couponCard(v, { viewer, csrf, back, link: true, shopUrl: m.homepage_url }))}` : h`<p class="empty">No product deals for ${m.name} right now.</p>`}
+${deals.length ? h`${deals.slice(0, shown).map((v) => couponCard(v, { viewer, csrf, back, link: true, shopUrl: m.homepage_url }))}
+${shown < deals.length ? h`<p class="cpn-more"><a class="ovk-btn ovk-btn--quiet" href="${base}?tab=deals&deals=${String(Math.min(deals.length, shown + DEALS_PAGE))}">Show ${String(Math.min(DEALS_PAGE, deals.length - shown))} more <span class="cpn-muted">(${String(shown)} of ${String(deals.length)})</span></a></p>` : ''}` : h`<p class="empty">No product deals for ${m.name} right now.</p>`}
${merchantTrend(success, m.name)}`;
${merchantTrend(success, m.name)}`;
} else if (!codes.length) {
} else if (!codes.length) {
// A shop with no codes says so, and offers the watch that tells us when one appears.
// A shop with no codes says so, and offers the watch that tells us when one appears.
@@ -148,6 +148,25 @@ function bestCardCode(html) {
@@ -148,6 +148,25 @@ function bestCardCode(html) {
assert.doesNotMatch(about.text, /verified/i);
assert.doesNotMatch(about.text, /verified/i);
});
});
+ await check('product deals show twenty at a time, with a link to the next ones (Amazon had 200 on one page)', async () => {
+ const shop = await t.merchant({ name: 'Deals Shop', host: 'deals-shop.com' });
+ const people = Array.from({ length: 5 }, (_, i) => t.network.addUser('dealer-' + i));
+ let n = 0;
+ while (n < 25) {
+ await submitCode(t, people[n % 5], shop.slug, 'DEAL' + String(n).padStart(3, '0'), 'Widget ' + n + ' for $' + (10 + n));
+ n++;
+ }
+ const first = await t.get('/m/' + shop.slug + '?tab=deals');
+ assert.strictEqual(first.status, 200);
+ assert.strictEqual((first.text.match(/One product/g) || []).length, 20, 'the first page shows twenty');
+ assert.match(first.text, /href="\/m\/[^"]+\?tab=deals&deals=25">Show 5 more/, 'and a link to the rest');
+ const all = await t.get('/m/' + shop.slug + '?tab=deals&deals=25');
+ assert.strictEqual((all.text.match(/One product/g) || []).length, 25, 'the link shows them all');
+ assert.doesNotMatch(all.text, /Show \d+ more/, 'with nothing left to show, no link');
+ const silly = await t.get('/m/' + shop.slug + '?tab=deals&deals=-4');
+ assert.strictEqual((silly.text.match(/One product/g) || []).length, 20, 'a nonsense count falls back to a page');
+ });
+
await t.close();
await t.close();
done();
done();
})();
})();