580293867d
Deletes the pre-Astro pages, scripts, and stylesheets that the migration replaced, and moves the ones it did not replace out of the way. Deleted (32 files): app.js, responsive.css, landing.css, rules/app.js, rules/styles.css, skills/app.js, the ten route index.html files, and the root hands-on/ copy, which is byte-identical to public/hands-on/ -- the one the build actually ships. Moved to legacy/ (12 files): styles.css, full-guide/audit.css, chapters.css, skills/styles.css, skills-review/styles.css, skills-review/change-lens.css, and the skills-review/app.js module graph. These are not dead. The Astro pages import them and the build fails without them, which the plan had not accounted for. They go to legacy/ rather than src/ because check-tokens.mjs sweeps src, and these files are full of raw hex and unnamed breakpoints: moving one into src/ should mean migrating it to tokens in the same change, not adding a scan exclusion. The prettier, stylelint, and eslint ignore lists that already named these files at their old paths now name legacy/ instead. verify.mjs no longer reads app.js. The 102 Portuguese strings were extracted from its translations.pt object before deletion into .agents/snapshots/full-guide-pt.json -- a legacy capture, not a snapshot of the Astro build, so the assertion still compares against an independent source. The brace-matching helper's assertion is replaced by one that rejects an empty snapshot entry, without which trimming the snapshot would make the presence check pass vacuously. Count stays at 84. audit-ui.mjs reads the ten pages from dist/ and resolves Astro's base-absolute hrefs against it. Before deleting anything, rendered-text-diff was run across all ten routes plus both Portuguese pages: every one at parity, 0 missing and 0 extra. That comparison is not repeatable once the legacy files are gone. computed-style-diff on /full-guide/ stays at 32 differences, so the moves are style-neutral. Docs updated to match: README, AGENTS.md, GATES.md, the architecture context, the operations guide's lab instructions, and the three skills that told you to serve the vanilla site. Publishing is not part of this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
3.3 KiB
JavaScript
54 lines
3.3 KiB
JavaScript
// Reader vote widget: "which draft would you ship?" per reviewed skill.
|
|
// The page itself is static (Gitea Pages), so this talks to a small
|
|
// separate API — see /vote-service in the repository root. One vote per
|
|
// source is enforced server-side by IP, not here; this module only renders
|
|
// state and remembers the local choice so a returning visitor sees it
|
|
// without re-voting.
|
|
const API_BASE = (window.SKILLS_REVIEW_VOTE_API || '').replace(/\/$/, '');
|
|
const escape = (value) => value.replace(/[&<>"']/g, (character) => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[character]);
|
|
|
|
function voterId() {
|
|
let id = localStorage.getItem('skills-review-voter-id');
|
|
if (!id) { id = crypto.randomUUID(); localStorage.setItem('skills-review-voter-id', id); }
|
|
return id;
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const response = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'X-Voter-Id': voterId(), ...options.headers } });
|
|
if (!response.ok) throw new Error(`vote API ${response.status}`);
|
|
return response.json();
|
|
}
|
|
|
|
function widgetMarkup(skillId, tally, you, unavailable) {
|
|
const total = (tally.original || 0) + (tally.improved || 0);
|
|
const share = (count) => total ? Math.round((count / total) * 100) : 0;
|
|
if (unavailable) return `<section class="vote-widget" aria-label="Vote unavailable"><span>READER VOTE</span><p>Voting is offline right now — the vote service is not configured or unreachable.</p></section>`;
|
|
return `<section class="vote-widget" aria-label="Vote on this review" data-skill="${escape(skillId)}">
|
|
<span>WHICH DRAFT WOULD YOU SHIP?</span>
|
|
<div class="vote-buttons" role="group" aria-label="Cast your vote">
|
|
<button data-vote="original" aria-pressed="${you === 'original'}">Original<b>${tally.original || 0} · ${share(tally.original || 0)}%</b></button>
|
|
<button data-vote="improved" aria-pressed="${you === 'improved'}">Improved draft<b>${tally.improved || 0} · ${share(tally.improved || 0)}%</b></button>
|
|
</div>
|
|
<p class="vote-note">${you ? `You voted ${you === 'original' ? 'original' : 'improved draft'}. Pick the other option to change it.` : 'One vote per visitor, tracked by network source.'}</p>
|
|
</section>`;
|
|
}
|
|
|
|
export async function renderVoteWidget(container, skillId) {
|
|
if (!API_BASE) { container.innerHTML = widgetMarkup(skillId, {}, null, true); return; }
|
|
container.innerHTML = widgetMarkup(skillId, {}, null, false);
|
|
const cast = async (choice) => {
|
|
container.innerHTML = widgetMarkup(skillId, {}, null, false);
|
|
try {
|
|
const result = await api('/api/votes', { method: 'POST', body: JSON.stringify({ skillId, choice }) });
|
|
container.innerHTML = widgetMarkup(skillId, { original: result.original, improved: result.improved }, result.you, false);
|
|
bind();
|
|
} catch { container.innerHTML = widgetMarkup(skillId, {}, null, true); }
|
|
};
|
|
function bind() { container.querySelectorAll('[data-vote]').forEach((button) => button.addEventListener('click', () => cast(button.dataset.vote))); }
|
|
try {
|
|
const result = await api(`/api/votes?skillId=${encodeURIComponent(skillId)}`);
|
|
container.innerHTML = widgetMarkup(skillId, result.tallies?.[skillId] || {}, result.you, false);
|
|
} catch { container.innerHTML = widgetMarkup(skillId, {}, null, true); }
|
|
bind();
|
|
}
|