openvibers/openvibe.coupons Commits
Coupons: a code's page has its title as the h1 (it had none; the list card's h3 was reused) (#54)
Changes
@@@ -15,6 +15,7 @@ h3 { font-size: 1.1rem; margin: 0 0 .4rem; }.meta, .empty { color: var(--text-muted, #7386a3); font-size: .9rem; margin: .25rem 0; }.unknown { font-style: italic; color: var(--text-secondary, #96a7c2); }.coupon { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 12px 16px; margin: 12px 0; background: var(--bg-card, #131c2e); }++.coupon .coupon-title { font-size: 1.35rem; line-height: 1.3; margin: 4px 0 10px; }.code-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: .3rem 0; }.code-line .label { color: var(--text-muted, #7386a3); font-size: .85rem; }.code { font: 600 1.15rem/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 4px 10px; border: 1px dashed var(--accent, #3b82f6); border-radius: 6px; user-select: all; -webkit-user-select: all; word-break: break-all; }@@@ -88,10 +88,11 @@ function reportForms(v, { viewer, csrf, back }) {</div>`;}--/** One code. v: coupons.view(); opts: { viewer, csrf, back, link } */++/** One code. v: coupons.view(); opts: { viewer, csrf, back, link, page } (page: its own page, where the title is the h1) */function couponCard(v, opts = {}) {++ const title = opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title;return h`<article class="coupon status-${v.status}" id="${v.id}">--<h3>${opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title}</h3>++${opts.page ? h`<h1 class="coupon-title">${title}</h1>` : h`<h3>${title}</h3>`}<p class="code-line"><span class="label">Code</span> <code class="code">${v.code}</code> ${statusBadge(v.status)}</p>${v.description ? h`<p>${v.description}</p>` : ''}<p class="meta">${confidenceText(v)}</p>@@@ -162,7 +163,7 @@ function couponPage({ v, m, history, viewer, csrf, back, flashMsg }) {return h`${raw(ssr.breadcrumbsHtml([{ name: 'Coupons', url: '/' }, { name: m.name, url: `/m/${m.slug}` }, { name: v.code }]))}${flash(flashMsg)}${v.active ? '' : h`<p class="notice">This code is not in active results (${STATUS_LABEL[v.status] ? STATUS_LABEL[v.status].toLowerCase() : v.status}${v.expiry.known && !v.active && v.status !== 'expired' ? ', past its expiry' : ''}).</p>`}--${couponCard(v, { viewer, csrf, back })}++${couponCard(v, { viewer, csrf, back, page: true })}<h2>Status history</h2>${history.length ? h`<ol class="history">${history.map((x) => h`<li>${utc(new Date(x.created_at).toISOString())}: ${x.from_status ? h`${STATUS_LABEL[x.from_status] || x.from_status} → ` : ''}${STATUS_LABEL[x.to_status] || x.to_status}${x.confidence_after != null ? h`, confidence ${Math.round(x.confidence_after * 100)}%` : ''} <span class="meta">(${x.reason.startsWith('staff') ? 'staff' : x.reason.startsWith('service:') ? 'service' : x.reason})</span></li>`)}</ol>` : h`<p class="empty">No changes yet.</p>`}<p class="meta"><a href="/c/${v.id}.json">This code as JSON</a></p>`;@@@ -78,6 +78,8 @@ const { boot, check, done } = require('./helpers/boot');await check('JSON-LD only from real fields (breadcrumbs, web page); the JSON twin has the page\'s content', async () => {const page = (await t.get(`/c/${codeId}`)).text;++ assert.strictEqual((page.match(/<h1\b/g) || []).length, 1, 'the code page has one h1: its title');++ assert.match(page, /<h1 class="coupon-title">/);const blocks = [...page.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));assert.deepStrictEqual(blocks.map((b) => b['@type']).sort(), ['BreadcrumbList', 'WebPage']);const json = (await t.get(`/c/${codeId}.json`)).json();
@@ -15,6 +15,7 @@ h3 { font-size: 1.1rem; margin: 0 0 .4rem; }
@@ -15,6 +15,7 @@ h3 { font-size: 1.1rem; margin: 0 0 .4rem; }
.meta, .empty { color: var(--text-muted, #7386a3); font-size: .9rem; margin: .25rem 0; }
.meta, .empty { color: var(--text-muted, #7386a3); font-size: .9rem; margin: .25rem 0; }
.unknown { font-style: italic; color: var(--text-secondary, #96a7c2); }
.unknown { font-style: italic; color: var(--text-secondary, #96a7c2); }
.coupon { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 12px 16px; margin: 12px 0; background: var(--bg-card, #131c2e); }
.coupon { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 12px 16px; margin: 12px 0; background: var(--bg-card, #131c2e); }
+.coupon .coupon-title { font-size: 1.35rem; line-height: 1.3; margin: 4px 0 10px; }
.code-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: .3rem 0; }
.code-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: .3rem 0; }
.code-line .label { color: var(--text-muted, #7386a3); font-size: .85rem; }
.code-line .label { color: var(--text-muted, #7386a3); font-size: .85rem; }
.code { font: 600 1.15rem/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 4px 10px; border: 1px dashed var(--accent, #3b82f6); border-radius: 6px; user-select: all; -webkit-user-select: all; word-break: break-all; }
.code { font: 600 1.15rem/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 4px 10px; border: 1px dashed var(--accent, #3b82f6); border-radius: 6px; user-select: all; -webkit-user-select: all; word-break: break-all; }
@@ -88,10 +88,11 @@ function reportForms(v, { viewer, csrf, back }) {
@@ -88,10 +88,11 @@ function reportForms(v, { viewer, csrf, back }) {
</div>`;
</div>`;
}
}
-/** One code. v: coupons.view(); opts: { viewer, csrf, back, link } */
+/** One code. v: coupons.view(); opts: { viewer, csrf, back, link, page } (page: its own page, where the title is the h1) */
function couponCard(v, opts = {}) {
function couponCard(v, opts = {}) {
+ const title = opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title;
return h`<article class="coupon status-${v.status}" id="${v.id}">
return h`<article class="coupon status-${v.status}" id="${v.id}">
-<h3>${opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title}</h3>
+${opts.page ? h`<h1 class="coupon-title">${title}</h1>` : h`<h3>${title}</h3>`}
<p class="code-line"><span class="label">Code</span> <code class="code">${v.code}</code> ${statusBadge(v.status)}</p>
<p class="code-line"><span class="label">Code</span> <code class="code">${v.code}</code> ${statusBadge(v.status)}</p>
${v.description ? h`<p>${v.description}</p>` : ''}
${v.description ? h`<p>${v.description}</p>` : ''}
<p class="meta">${confidenceText(v)}</p>
<p class="meta">${confidenceText(v)}</p>
@@ -162,7 +163,7 @@ function couponPage({ v, m, history, viewer, csrf, back, flashMsg }) {
@@ -162,7 +163,7 @@ function couponPage({ v, m, history, viewer, csrf, back, flashMsg }) {
return h`${raw(ssr.breadcrumbsHtml([{ name: 'Coupons', url: '/' }, { name: m.name, url: `/m/${m.slug}` }, { name: v.code }]))}
return h`${raw(ssr.breadcrumbsHtml([{ name: 'Coupons', url: '/' }, { name: m.name, url: `/m/${m.slug}` }, { name: v.code }]))}
${flash(flashMsg)}
${flash(flashMsg)}
${v.active ? '' : h`<p class="notice">This code is not in active results (${STATUS_LABEL[v.status] ? STATUS_LABEL[v.status].toLowerCase() : v.status}${v.expiry.known && !v.active && v.status !== 'expired' ? ', past its expiry' : ''}).</p>`}
${v.active ? '' : h`<p class="notice">This code is not in active results (${STATUS_LABEL[v.status] ? STATUS_LABEL[v.status].toLowerCase() : v.status}${v.expiry.known && !v.active && v.status !== 'expired' ? ', past its expiry' : ''}).</p>`}
-${couponCard(v, { viewer, csrf, back })}
+${couponCard(v, { viewer, csrf, back, page: true })}
<h2>Status history</h2>
<h2>Status history</h2>
${history.length ? h`<ol class="history">${history.map((x) => h`<li>${utc(new Date(x.created_at).toISOString())}: ${x.from_status ? h`${STATUS_LABEL[x.from_status] || x.from_status} → ` : ''}${STATUS_LABEL[x.to_status] || x.to_status}${x.confidence_after != null ? h`, confidence ${Math.round(x.confidence_after * 100)}%` : ''} <span class="meta">(${x.reason.startsWith('staff') ? 'staff' : x.reason.startsWith('service:') ? 'service' : x.reason})</span></li>`)}</ol>` : h`<p class="empty">No changes yet.</p>`}
${history.length ? h`<ol class="history">${history.map((x) => h`<li>${utc(new Date(x.created_at).toISOString())}: ${x.from_status ? h`${STATUS_LABEL[x.from_status] || x.from_status} → ` : ''}${STATUS_LABEL[x.to_status] || x.to_status}${x.confidence_after != null ? h`, confidence ${Math.round(x.confidence_after * 100)}%` : ''} <span class="meta">(${x.reason.startsWith('staff') ? 'staff' : x.reason.startsWith('service:') ? 'service' : x.reason})</span></li>`)}</ol>` : h`<p class="empty">No changes yet.</p>`}
<p class="meta"><a href="/c/${v.id}.json">This code as JSON</a></p>`;
<p class="meta"><a href="/c/${v.id}.json">This code as JSON</a></p>`;
@@ -78,6 +78,8 @@ const { boot, check, done } = require('./helpers/boot');
@@ -78,6 +78,8 @@ const { boot, check, done } = require('./helpers/boot');
await check('JSON-LD only from real fields (breadcrumbs, web page); the JSON twin has the page\'s content', async () => {
await check('JSON-LD only from real fields (breadcrumbs, web page); the JSON twin has the page\'s content', async () => {
const page = (await t.get(`/c/${codeId}`)).text;
const page = (await t.get(`/c/${codeId}`)).text;
+ assert.strictEqual((page.match(/<h1\b/g) || []).length, 1, 'the code page has one h1: its title');
+ assert.match(page, /<h1 class="coupon-title">/);
const blocks = [...page.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
const blocks = [...page.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
assert.deepStrictEqual(blocks.map((b) => b['@type']).sort(), ['BreadcrumbList', 'WebPage']);
assert.deepStrictEqual(blocks.map((b) => b['@type']).sort(), ['BreadcrumbList', 'WebPage']);
const json = (await t.get(`/c/${codeId}.json`)).json();
const json = (await t.get(`/c/${codeId}.json`)).json();