'use client'; import { useRef } from 'react'; import { trackShareCardGenerated } from '@/lib/analytics'; interface ShareCardProps { sport: 'NBA' | 'MLB' | 'WNBA'; player: string; stat: string; line: number; direction: 'over' | 'under'; grade: string; projection?: number; sampleSize?: number; } const SPORT_COLOR: Record = { NBA: '#E94B3C', MLB: '#1E90FF', WNBA: '#FFB347', }; function gradeColor(grade: string): string { const g = (grade || '').trim().toUpperCase().charAt(0); if (g === 'A') return '#00C896'; if (g === 'B') return '#4A9EFF'; if (g === 'C') return '#FFB347'; return '#FF6B6B'; } /** * Renders a 1200x630 OG-shaped share image into a hidden canvas, then * provides Download + Copy actions. Intentionally hides the analysis — * shares the GRADE only, which is what drives traffic back to the site. */ export function useShareCard(props: ShareCardProps) { const canvasRef = useRef(null); const ensureCanvas = (): HTMLCanvasElement => { if (canvasRef.current) return canvasRef.current; const c = document.createElement('canvas'); c.width = 1200; c.height = 630; canvasRef.current = c; return c; }; const renderToCanvas = async (): Promise => { const c = ensureCanvas(); const ctx = c.getContext('2d'); if (!ctx) throw new Error('No 2D context.'); // Background — obsidian with diagonal accent ctx.fillStyle = '#0A0A0F'; ctx.fillRect(0, 0, c.width, c.height); // Diagonal gradient overlay const g = ctx.createLinearGradient(0, 0, c.width, c.height); g.addColorStop(0, 'rgba(26,74,58,0.20)'); g.addColorStop(1, 'rgba(0,200,150,0.02)'); ctx.fillStyle = g; ctx.fillRect(0, 0, c.width, c.height); // VYNDR wordmark top-left ctx.fillStyle = '#F0F0F5'; ctx.font = '800 38px "JetBrains Mono", "SF Mono", ui-monospace, monospace'; ctx.fillText('VYND', 64, 96); ctx.fillStyle = '#00D4A0'; ctx.fillText('R', 64 + ctx.measureText('VYND').width, 96); // Sport badge const sportColor = SPORT_COLOR[props.sport]; ctx.font = '700 16px "JetBrains Mono", monospace'; ctx.fillStyle = sportColor; ctx.fillText(props.sport, 64, 220); // Player name (large) ctx.fillStyle = '#F0F0F5'; ctx.font = '700 72px "Instrument Sans", system-ui, sans-serif'; wrapText(ctx, props.player, 64, 300, 780, 80); // Prop line (mono) ctx.fillStyle = '#8A8A9A'; ctx.font = '500 28px "JetBrains Mono", monospace'; const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); ctx.fillText( `${cap(props.direction)} ${props.line} ${props.stat.replace(/_/g, ' ')}`, 64, 420, ); // Grade letter (huge, colored) const gc = gradeColor(props.grade); ctx.fillStyle = gc; ctx.font = '800 240px "JetBrains Mono", monospace'; ctx.textAlign = 'right'; ctx.fillText(props.grade || '—', c.width - 64, 380); // Glow ring behind the grade ctx.shadowColor = gc; ctx.shadowBlur = 60; ctx.fillText(props.grade || '—', c.width - 64, 380); ctx.shadowBlur = 0; // Projection (small, beneath player line) if (props.projection != null) { ctx.textAlign = 'left'; ctx.fillStyle = '#5A5A6A'; ctx.font = '500 22px "JetBrains Mono", monospace'; ctx.fillText(`Projection ${props.projection.toFixed(1)}`, 64, 470); } // Footer — watermark ctx.textAlign = 'left'; ctx.fillStyle = '#5A5A6A'; ctx.font = '500 18px "JetBrains Mono", monospace'; ctx.fillText('vyndr.app', 64, 580); ctx.textAlign = 'right'; ctx.fillStyle = '#5A5A6A'; ctx.fillText('Built in Detroit.', c.width - 64, 580); return c; }; const download = async () => { const c = await renderToCanvas(); c.toBlob((blob) => { if (!blob) return; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `vyndr-${props.player.replace(/\W+/g, '-')}-${props.grade}.png`.toLowerCase(); document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); trackShareCardGenerated({ sport: props.sport, grade: props.grade }); }, 'image/png'); }; const copyToClipboard = async (): Promise => { if (typeof ClipboardItem === 'undefined' || !navigator.clipboard?.write) return false; const c = await renderToCanvas(); return new Promise((resolve) => { c.toBlob(async (blob) => { if (!blob) return resolve(false); try { await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); trackShareCardGenerated({ sport: props.sport, grade: props.grade }); resolve(true); } catch { resolve(false); } }, 'image/png'); }); }; return { download, copyToClipboard }; } function wrapText( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number, ) { const words = text.split(' '); let line = ''; for (const word of words) { const test = line ? `${line} ${word}` : word; const m = ctx.measureText(test); if (m.width > maxWidth && line) { ctx.fillText(line, x, y); line = word; y += lineHeight; } else { line = test; } } if (line) ctx.fillText(line, x, y); }