// The club feed page. Server-rendered, self-contained: no external requests, // system fonts only, matching the rest of pds.js. /** * @param {string} s */ const esc = (s) => String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); /** @param {number} meters */ const km = (meters) => (meters / 1000).toFixed(1); /** @param {number} seconds */ function duration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; const ms = `${String(m).padStart(h ? 2 : 1, '0')}:${String(s).padStart(2, '0')}`; return h ? `${h}:${ms}` : ms; } /** @param {number} meters @param {number} seconds */ function pace(meters, seconds) { const secPerKm = seconds / (meters / 1000); const m = Math.floor(secPerKm / 60); const s = Math.round(secPerKm % 60); return `${m}:${String(s).padStart(2, '0')}/km`; } /** @param {string} iso */ function dayLabel(iso) { return new Date(iso).toLocaleDateString('en-US', { weekday: 'short' }); } /** * @param {Object} feed * @param {string} feed.clubName * @param {string} feed.space * @param {Array<{did: string, handle: string, rev: string, hashHex: string, runs: Array<{startedAt: string, distanceMeters: number, durationSec: number, note: string}>}>} feed.members * @returns {string} */ export function renderFeedPage({ clubName, space, members }) { const totalMeters = members .flatMap((m) => m.runs) .reduce((sum, r) => sum + r.distanceMeters, 0); const maxMeters = Math.max( ...members.map((m) => m.runs.reduce((sum, r) => sum + r.distanceMeters, 0)), 1, ); // The lane board: each member is a track lane, bar length is their share of // the week's distance. Sorted like a results board, most distance first. const lanes = members .map((m) => ({ ...m, meters: m.runs.reduce((sum, r) => sum + r.distanceMeters, 0), })) .sort((a, b) => b.meters - a.meters) .map( (m, i) => `
${i + 1} ${esc(m.handle)} ${km(m.meters)} km
`, ) .join(''); const runs = members .flatMap((m) => m.runs.map((r) => ({ ...r, handle: m.handle }))) .sort((a, b) => b.startedAt.localeCompare(a.startedAt)) .map( (r) => `
${dayLabel(r.startedAt)} ${esc(r.handle)} ${esc(r.note)} ${km(r.distanceMeters)} km ${duration(r.durationSec)} ${pace(r.distanceMeters, r.durationSec)}
`, ) .join(''); // The receipts: this feed was assembled by verifying each member's commit // against its MAC, so show what was checked. This is the point of the // example. const receipts = members .map( (m) => `
✓ ${esc(m.handle)} ${esc(m.did)} rev ${esc(m.rev)} hash ${esc(m.hashHex.slice(0, 12))}…
`, ) .join(''); return ` ${esc(clubName)}
${esc(space)}

${esc(clubName)}

This week the club ran ${km(totalMeters)} km together.

Lanes · this week
${lanes}
Runs
${runs}
Synced & verified

Each member's repo was pulled from their own PDS and verified before rendering: the commit's MAC, and the records against its set hash.

${receipts}
`; }