Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+381
View File
@@ -0,0 +1,381 @@
/**
* Share-card SVG renderer.
*
* Five templates: standard (grade), victory ("I told you"), recap (multi),
* cheatsheet (grid), gotd (Grade of the Day).
*
* Three sizes:
* twitter — 1200x675 (X cards, OG)
* story — 1080x1920 (IG Story, TikTok)
* square — 1080x1080 (IG feed, Discord embed)
*
* The renderer composes a single SVG string (no DOM, no user-controlled
* markup — every dynamic value passes through escapeXml first) and pipes
* it through sharp for PNG. If sharp's native binding is unavailable in
* a degraded environment, the route returns the raw SVG with
* Content-Type: image/svg+xml.
*
* SECURITY: all caller-supplied strings are XML-escaped. No <foreignObject>,
* no <script>, no <use href> external refs. Templates use only plain SVG
* primitives (rect, text, path, circle, line, g, defs, gradient, filter).
*/
const { COLORS, FONTS, SPORT_LABEL_COLOR, gradeColor } = require('./tokens');
const SIZES = Object.freeze({
twitter: { w: 1200, h: 675 },
story: { w: 1080, h: 1920 },
square: { w: 1080, h: 1080 },
});
const VALID_TYPES = new Set(['grade', 'victory', 'recap', 'cheatsheet', 'gotd']);
// ── tiny helpers ─────────────────────────────────────────────────────────
function escapeXml(input) {
if (input == null) return '';
return String(input)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function clampLen(s, max) {
if (s == null) return '';
const str = String(s);
return str.length > max ? str.slice(0, max - 1) + '…' : str;
}
function gradeGlowFilter(id, color) {
return `
<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="b1"/>
<feFlood flood-color="${color}" flood-opacity="0.75"/>
<feComposite in2="b1" operator="in" result="glow"/>
<feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>`;
}
function scanLinesPattern(id) {
// Subtle CRT scan lines, drawn at 4% opacity, repeating every 4px.
return `
<pattern id="${id}" width="4" height="4" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="2000" y2="0" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
</pattern>`;
}
function diagonalSlash() {
// The signature -2deg diagonal cut, drawn faintly.
return `
<linearGradient id="slash" x1="0" y1="0" x2="1" y2="0.07">
<stop offset="0" stop-color="rgba(0,212,160,0)"/>
<stop offset="0.5" stop-color="rgba(0,212,160,0.08)"/>
<stop offset="1" stop-color="rgba(0,212,160,0)"/>
</linearGradient>`;
}
function commonDefs(width, height) {
return `<defs>
${diagonalSlash()}
${scanLinesPattern('scan')}
${gradeGlowFilter('glow-aplus', COLORS.gradeAplus)}
${gradeGlowFilter('glow-a', COLORS.gradeA)}
${gradeGlowFilter('glow-b', COLORS.gradeB)}
${gradeGlowFilter('glow-c', COLORS.gradeC)}
${gradeGlowFilter('glow-d', COLORS.gradeD)}
<linearGradient id="bgFade" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${COLORS.bg1}"/>
<stop offset="1" stop-color="${COLORS.bg0}"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.35" r="0.6">
<stop offset="0" stop-color="rgba(0,212,160,0.16)"/>
<stop offset="1" stop-color="rgba(0,212,160,0)"/>
</radialGradient>
</defs>`;
}
function baseBackground(width, height) {
// Background stack: gradient + radial glow + scan lines + slash band.
return `
<rect width="${width}" height="${height}" fill="url(#bgFade)"/>
<rect width="${width}" height="${height}" fill="url(#glow)"/>
<rect width="${width}" height="${height}" fill="url(#scan)"/>
<rect width="${width}" height="${height}" fill="url(#slash)"/>`;
}
function wordmark(x, y, fontSize) {
// VYNDR with green R + drop-shadow on the R. No external font load —
// SVG falls back through the font-family stack at rasterize time.
const rOffset = fontSize * 0.62; // approx width of "VYND" at this size
return `
<g font-family="${FONTS.mono}" font-weight="800" letter-spacing="${fontSize * 0.10}">
<text x="${x}" y="${y}" font-size="${fontSize}" fill="${COLORS.text0}">VYND</text>
<text x="${x + rOffset * 3.4}" y="${y}" font-size="${fontSize}" fill="${COLORS.gradeA}"
style="filter:drop-shadow(0 0 ${fontSize * 0.25}px rgba(0,212,160,0.7))">R</text>
</g>`;
}
function watermarkFooter(width, height, extra = '') {
return `
<text x="${width / 2}" y="${height - 32}" text-anchor="middle"
font-family="${FONTS.mono}" font-size="14" letter-spacing="5" fill="${COLORS.text2}">
VYNDR.XYZ · @GETVYNDR${extra ? ' · ' + escapeXml(extra) : ''}
</text>`;
}
function sportPill(x, y, sport) {
const key = String(sport || '').toLowerCase();
const color = SPORT_LABEL_COLOR[key] || COLORS.text1;
const label = (key || 'sport').toUpperCase();
// Simple pill: rounded rect + centered text.
const w = 90;
const h = 28;
return `
<g>
<rect x="${x}" y="${y}" rx="14" ry="14" width="${w}" height="${h}"
fill="${color}" fill-opacity="0.16" stroke="${color}" stroke-opacity="0.4"/>
<text x="${x + w / 2}" y="${y + h / 2 + 5}" text-anchor="middle"
font-family="${FONTS.mono}" font-size="13" font-weight="700"
letter-spacing="2" fill="${color}">${escapeXml(label)}</text>
</g>`;
}
function gradeBadge(x, y, grade, scale = 1) {
const safeGrade = clampLen(grade, 4) || '—';
const color = gradeColor(grade);
const filterId = ({
'A+': 'glow-aplus', 'A': 'glow-a', 'A-': 'glow-a',
'B+': 'glow-b', 'B': 'glow-b', 'B-': 'glow-b',
'C+': 'glow-c', 'C': 'glow-c', 'C-': 'glow-c',
'D': 'glow-d', 'F': 'glow-d',
})[safeGrade] || 'glow-b';
const size = 200 * scale;
return `
<text x="${x}" y="${y}" text-anchor="middle"
font-family="${FONTS.mono}" font-weight="800" font-size="${size}"
letter-spacing="-4" fill="${color}" filter="url(#${filterId})"
style="filter:drop-shadow(0 0 ${24 * scale}px ${color})">${escapeXml(safeGrade)}</text>`;
}
// ── templates ────────────────────────────────────────────────────────────
function renderStandardGrade({ width, height, player, sport, stat, line, direction, grade, projection, summary }) {
const titleY = height < 700 ? 200 : 280;
const gradeY = height < 700 ? 460 : 660;
return `
${baseBackground(width, height)}
${wordmark(60, 80, 44)}
${sportPill(width - 150, 56, sport)}
<text x="60" y="${titleY}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
${escapeXml(clampLen(player, 28))}
</text>
<text x="60" y="${titleY + 60}" font-family="${FONTS.mono}" font-size="32"
font-weight="500" fill="${COLORS.text1}">
${escapeXml(clampLen(stat, 14))} ${escapeXml(String(direction || 'over').toUpperCase())} ${escapeXml(String(line ?? '—'))}
</text>
${gradeBadge(width / 2, gradeY, grade, height < 700 ? 0.9 : 1.1)}
${projection != null ? `
<text x="60" y="${gradeY + 80}" font-family="${FONTS.mono}" font-size="22"
font-weight="700" fill="${COLORS.text1}" letter-spacing="3">
PROJECTION ${escapeXml(String(projection))}
</text>` : ''}
${summary ? `
<text x="60" y="${gradeY + 120}" font-family="${FONTS.sans}" font-size="20"
fill="${COLORS.text1}">
${escapeXml(clampLen(summary, 80))}
</text>` : ''}
${watermarkFooter(width, height)}`;
}
function renderVictoryCard(opts) {
const { width, height, result_actual, grade, line } = opts;
// Standard grade card body + a result banner across the top.
const safeActual = escapeXml(clampLen(result_actual || 'HIT', 32));
return `
${baseBackground(width, height)}
<rect x="0" y="0" width="${width}" height="80" fill="${COLORS.gradeA}" fill-opacity="0.18"/>
<text x="${width / 2}" y="52" text-anchor="middle"
font-family="${FONTS.mono}" font-weight="800" font-size="28"
letter-spacing="6" fill="${COLORS.gradeA}"
style="filter:drop-shadow(0 0 8px rgba(0,212,160,0.7))">
✓ HIT — ${safeActual}
</text>
${wordmark(60, 140, 40)}
${sportPill(width - 150, 110, opts.sport)}
<text x="60" y="${height < 700 ? 260 : 340}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
${escapeXml(clampLen(opts.player, 28))}
</text>
<text x="60" y="${(height < 700 ? 260 : 340) + 60}" font-family="${FONTS.mono}" font-size="32"
font-weight="500" fill="${COLORS.text1}">
${escapeXml(clampLen(opts.stat, 14))} ${escapeXml(String(opts.direction || 'over').toUpperCase())} ${escapeXml(String(line ?? '—'))}
</text>
${gradeBadge(width / 2, height - (height < 700 ? 180 : 260), grade, height < 700 ? 0.85 : 1.0)}
<text x="${width / 2}" y="${height - 70}" text-anchor="middle"
font-family="${FONTS.sans}" font-size="22" font-weight="600" fill="${COLORS.text1}">
VYNDR called it.
</text>
${watermarkFooter(width, height)}`;
}
function renderRecap({ width, height, date, entries = [], accuracy }) {
const safeEntries = entries.slice(0, 6);
const rowH = 64;
const startY = 240;
const rows = safeEntries.map((e, i) => {
const y = startY + i * rowH;
const ok = e.result === 'hit';
const tint = ok ? 'rgba(0,212,160,0.10)' : 'rgba(255,82,82,0.10)';
const mark = ok ? '✓' : '✗';
const markColor = ok ? COLORS.gradeA : COLORS.gradeD;
return `
<rect x="40" y="${y - 36}" width="${width - 80}" height="${rowH - 8}" rx="10" fill="${tint}"/>
<text x="64" y="${y}" font-family="${FONTS.mono}" font-size="28" font-weight="800" fill="${markColor}">${mark}</text>
<text x="100" y="${y}" font-family="${FONTS.sans}" font-size="24" font-weight="700" fill="${COLORS.text0}">
${escapeXml(clampLen(e.player, 22))}
</text>
<text x="${width / 2}" y="${y}" font-family="${FONTS.mono}" font-size="22" fill="${COLORS.text1}">
${escapeXml(clampLen(`${e.stat || ''} ${String(e.direction || '').toUpperCase()} ${e.line ?? ''}`, 28))}
</text>
<text x="${width - 220}" y="${y}" font-family="${FONTS.mono}" font-size="22" font-weight="800"
fill="${gradeColor(e.grade)}">${escapeXml(clampLen(e.grade, 4))}</text>
<text x="${width - 64}" y="${y}" text-anchor="end" font-family="${FONTS.mono}" font-size="22"
font-weight="700" fill="${ok ? COLORS.gradeA : COLORS.gradeD}">
${ok ? 'HIT' : 'MISS'}
</text>`;
}).join('');
const hits = safeEntries.filter((e) => e.result === 'hit').length;
const acc = accuracy != null ? accuracy : (safeEntries.length ? Math.round((hits / safeEntries.length) * 100) : 0);
return `
${baseBackground(width, height)}
${wordmark(60, 80, 36)}
<text x="60" y="140" font-family="${FONTS.mono}" font-size="22" font-weight="700"
letter-spacing="4" fill="${COLORS.text2}">LAST NIGHT'S RESULTS</text>
<text x="60" y="180" font-family="${FONTS.sans}" font-size="28" fill="${COLORS.text0}">
${escapeXml(clampLen(date || '', 32))}
</text>
${rows}
<text x="${width / 2}" y="${height - 90}" text-anchor="middle"
font-family="${FONTS.mono}" font-size="36" font-weight="800"
fill="${COLORS.gradeA}" letter-spacing="4"
style="filter:drop-shadow(0 0 10px rgba(0,212,160,0.6))">
ACCURACY ${acc}% (${hits}/${safeEntries.length})
</text>
${watermarkFooter(width, height)}`;
}
function renderCheatsheet({ width, height, date, gameCount, grades = [] }) {
const safe = grades.slice(0, 8);
const rowH = 60;
const startY = 240;
const rows = safe.map((g, i) => {
const y = startY + i * rowH;
return `
<text x="60" y="${y}" font-family="${FONTS.mono}" font-size="32" font-weight="800"
fill="${gradeColor(g.grade)}"
style="filter:drop-shadow(0 0 6px ${gradeColor(g.grade)})">${escapeXml(clampLen(g.grade, 4))}</text>
<text x="160" y="${y}" font-family="${FONTS.sans}" font-size="24" font-weight="600" fill="${COLORS.text0}">
${escapeXml(clampLen(g.player, 22))}
</text>
<text x="${width - 80}" y="${y}" text-anchor="end" font-family="${FONTS.mono}" font-size="22"
fill="${COLORS.text1}">
${escapeXml(clampLen(`${g.stat || ''} ${String(g.direction || '').toUpperCase()} ${g.line ?? ''}`, 28))}
</text>`;
}).join('');
return `
${baseBackground(width, height)}
${wordmark(60, 80, 36)}
<text x="60" y="140" font-family="${FONTS.mono}" font-size="22" font-weight="700"
letter-spacing="4" fill="${COLORS.gradeA}">TONIGHT'S CHEATSHEET</text>
<text x="60" y="180" font-family="${FONTS.sans}" font-size="22" fill="${COLORS.text1}">
${escapeXml(clampLen(date || '', 24))} · ${escapeXml(String(gameCount ?? 0))} games
</text>
${rows}
${watermarkFooter(width, height)}`;
}
function renderGOTD(opts) {
// Same shape as standard grade but with the "GRADE OF THE DAY" slug.
const { width, height } = opts;
return `
${baseBackground(width, height)}
<rect x="0" y="0" width="${width}" height="64" fill="${COLORS.gradeA}" fill-opacity="0.14"/>
<text x="${width / 2}" y="42" text-anchor="middle"
font-family="${FONTS.mono}" font-weight="800" font-size="22"
letter-spacing="8" fill="${COLORS.gradeA}">◆ GRADE OF THE DAY</text>
${wordmark(60, 130, 40)}
${sportPill(width - 150, 100, opts.sport)}
<text x="60" y="${height < 700 ? 250 : 320}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
${escapeXml(clampLen(opts.player, 28))}
</text>
<text x="60" y="${(height < 700 ? 250 : 320) + 56}" font-family="${FONTS.mono}" font-size="30"
fill="${COLORS.text1}">
${escapeXml(clampLen(opts.stat, 14))} ${escapeXml(String(opts.direction || 'over').toUpperCase())} ${escapeXml(String(opts.line ?? '—'))}
</text>
${gradeBadge(width / 2, height - (height < 700 ? 180 : 260), opts.grade, height < 700 ? 0.95 : 1.15)}
${opts.summary ? `
<text x="${width / 2}" y="${height - 110}" text-anchor="middle"
font-family="${FONTS.sans}" font-size="22" fill="${COLORS.text0}">
${escapeXml(clampLen(opts.summary, 96))}
</text>` : ''}
${watermarkFooter(width, height)}`;
}
// ── public API ───────────────────────────────────────────────────────────
function buildSvg(type, format, payload) {
if (!VALID_TYPES.has(type)) throw new Error(`unknown card type: ${type}`);
const sz = SIZES[format] || SIZES.twitter;
const body = (() => {
switch (type) {
case 'grade': return renderStandardGrade({ ...payload, width: sz.w, height: sz.h });
case 'victory': return renderVictoryCard({ ...payload, width: sz.w, height: sz.h });
case 'recap': return renderRecap({ ...payload, width: sz.w, height: sz.h });
case 'cheatsheet': return renderCheatsheet({ ...payload, width: sz.w, height: sz.h });
case 'gotd': return renderGOTD({ ...payload, width: sz.w, height: sz.h });
default: throw new Error(`unhandled card type: ${type}`);
}
})();
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sz.w} ${sz.h}" width="${sz.w}" height="${sz.h}">
${commonDefs(sz.w, sz.h)}
${body}
</svg>`;
}
async function rasterize(svg) {
// Lazy require so a missing sharp binding doesn't sink the whole module.
let sharp;
try { sharp = require('sharp'); }
catch (err) {
const e = new Error('sharp unavailable');
e.code = 'SHARP_UNAVAILABLE';
throw e;
}
return sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer();
}
module.exports = {
SIZES,
VALID_TYPES,
buildSvg,
rasterize,
// exported for tests
escapeXml,
clampLen,
};
+64
View File
@@ -0,0 +1,64 @@
/**
* Shared design tokens for SVG card rendering.
* Mirror of the values in web/src/app/globals.css so social-card output
* matches the in-app brand exactly.
*/
const COLORS = Object.freeze({
bg0: '#06060B',
bg1: '#0E0E16',
bg2: '#15151F',
bg3: '#1A1A26',
text0: '#E8E8F0',
text1: '#7A7A8E',
text2: '#4A4A5E',
border: '#1E1E2E',
borderHi: '#2A2A3E',
acc0: '#0F3D2E',
acc1: '#1A5A42',
gradeAplus: '#00FFB8',
gradeA: '#00D4A0',
gradeB: '#4A9EFF',
gradeC: '#FFB347',
gradeD: '#FF5252',
crimson: '#8B0000',
nba: '#E94B3C',
mlb: '#1E90FF',
wnba: '#F7944A',
nfl: '#013369',
nhl: '#A0A0B0',
tennis: '#C5B358',
mma: '#D4AF37',
boxing: '#8B0000',
golf: '#2E7D32',
});
const FONTS = Object.freeze({
mono: "'IBM Plex Mono','JetBrains Mono','SF Mono',ui-monospace,monospace",
sans: "'Instrument Sans','Helvetica Neue',-apple-system,sans-serif",
});
const SPORT_LABEL_COLOR = Object.freeze({
nba: COLORS.nba,
wnba: COLORS.wnba,
mlb: COLORS.mlb,
nfl: COLORS.nfl,
nhl: COLORS.nhl,
tennis: COLORS.tennis,
mma: COLORS.mma,
boxing: COLORS.boxing,
golf: COLORS.golf,
});
function gradeColor(grade) {
if (!grade) return COLORS.text1;
const head = String(grade).trim().toUpperCase();
if (head.startsWith('A+')) return COLORS.gradeAplus;
if (head.startsWith('A')) return COLORS.gradeA;
if (head.startsWith('B')) return COLORS.gradeB;
if (head.startsWith('C')) return COLORS.gradeC;
if (head.startsWith('D') || head.startsWith('F')) return COLORS.gradeD;
return COLORS.text1;
}
module.exports = { COLORS, FONTS, SPORT_LABEL_COLOR, gradeColor };