/** * V2 social card renderer — satori (HTML/CSS → SVG) → resvg (SVG → PNG). * Modern editorial design. Uses React.createElement (h) form for satori's * strict layout requirements (every multi-child node needs explicit display). */ import satori from "satori"; import { Resvg } from "@resvg/resvg-js"; import * as fs from "fs"; import * as path from "path"; const OUTPUT_DIR = path.join(process.cwd(), "public", "predictions"); const ARIAL = fs.readFileSync("C:/Windows/Fonts/arial.ttf"); const ARIAL_BOLD = fs.readFileSync("C:/Windows/Fonts/arialbd.ttf"); const ARIAL_BLACK = fs.existsSync("C:/Windows/Fonts/ariblk.ttf") ? fs.readFileSync("C:/Windows/Fonts/ariblk.ttf") : ARIAL_BOLD; interface Pick { market: string; pick: string; confidence: number; odds: number; } interface Card { sport: "football" | "basketball"; leagueName: string; countryName: string; matchDate: string; homeTeam: string; awayTeam: string; htScore: string; ftScore: string; scoreConfidence: number; picks: Pick[]; } // satori expects React-element shape: { type, props: { children, style, ... } } type N = string | { type: string; props: { style?: any; children?: any } }; const h = (type: string, style: any, ...children: N[]): N => ({ type, props: { style, children: children.length === 1 ? children[0] : children }, }); function buildCard(c: Card): N { const isBasket = c.sport === "basketball"; const accent = isBasket ? "#F59E0B" : "#22C55E"; const accentDark = isBasket ? "#92400E" : "#14532D"; const away = "#3B82F6"; const awayDark = "#1E3A8A"; const sportLabel = isBasket ? "BASKETBOL" : "FUTBOL"; const halfLabel = isBasket ? "İLK DEVRE" : "İLK YARI"; const avatar = (name: string, c1: string, c2: string) => h("div", { display: "flex", alignItems: "center", justifyContent: "center", width: 200, height: 200, borderRadius: 200, background: `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`, fontSize: 120, fontWeight: 900, color: "white", boxShadow: "0 25px 60px rgba(0,0,0,0.55), inset 0 -8px 0 rgba(0,0,0,0.22)", }, (name.match(/\p{L}/u)?.[0] || "?").toUpperCase()); const teamColumn = (name: string, c1: string, c2: string) => h("div", { display: "flex", flexDirection: "column", alignItems: "center", width: 300, }, avatar(name, c1, c2), h("div", { display: "flex", marginTop: 22, fontSize: 32, fontWeight: 900, color: "white", textAlign: "center", lineHeight: 1.05, maxWidth: 300, justifyContent: "center", }, name), ); const scoreBlock = h("div", { display: "flex", flexDirection: "column", alignItems: "center", }, h("div", { display: "flex", fontSize: 14, color: "rgba(255,255,255,0.4)", letterSpacing: 4, fontWeight: 700, marginBottom: 8, }, "MAÇ SONU"), h("div", { display: "flex", fontSize: 110, fontWeight: 900, color: "white", lineHeight: 1, letterSpacing: -4, }, c.ftScore), h("div", { display: "flex", marginTop: 16, fontSize: 18, color: accent, fontWeight: 800, background: `${accent}22`, padding: "8px 16px", borderRadius: 10, letterSpacing: 1, }, `${halfLabel} ${c.htScore}`), ); const pickRow = (p: Pick, idx: number) => h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px 26px", marginBottom: 12, background: "rgba(255,255,255,0.045)", borderLeft: `5px solid ${accent}`, borderRadius: 12, }, h("div", { display: "flex", alignItems: "center", flexGrow: 1 }, h("div", { display: "flex", justifyContent: "center", alignItems: "center", width: 44, height: 44, marginRight: 18, borderRadius: 8, background: `${accent}22`, color: accent, fontSize: 24, fontWeight: 900, }, String(idx + 1)), h("div", { display: "flex", flexDirection: "column" }, h("div", { display: "flex", fontSize: 28, fontWeight: 800, color: "white", lineHeight: 1.1 }, p.market), h("div", { display: "flex", marginTop: 4, fontSize: 18, color: "rgba(255,255,255,0.5)" }, `Oran ${p.odds.toFixed(2)}`), ), ), h("div", { display: "flex", alignItems: "center", justifyContent: "center", fontSize: 38, fontWeight: 900, color: accent, background: `${accent}15`, padding: "6px 18px", borderRadius: 10, border: `1px solid ${accent}`, }, `%${p.confidence}`), ); return h("div", { width: 1080, height: 1080, display: "flex", flexDirection: "column", padding: "50px 60px", background: "linear-gradient(180deg, #0F172A 0%, #020617 100%)", color: "white", fontFamily: "Arial", }, // HEADER h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 36, }, h("div", { display: "flex", alignItems: "center" }, h("div", { display: "flex", width: 8, height: 44, background: accent, marginRight: 16, borderRadius: 4, }), h("div", { display: "flex", flexDirection: "column" }, h("div", { display: "flex", fontSize: 14, color: "rgba(255,255,255,0.5)", letterSpacing: 3, fontWeight: 700, }, `${sportLabel} · ${c.countryName.toUpperCase()}`), h("div", { display: "flex", marginTop: 2, fontSize: 26, fontWeight: 900, color: "white", }, c.leagueName), ), ), h("div", { display: "flex", fontSize: 16, color: "rgba(255,255,255,0.6)", background: "rgba(255,255,255,0.05)", padding: "10px 16px", borderRadius: 10, border: "1px solid rgba(255,255,255,0.1)", fontWeight: 600, }, c.matchDate), ), // TEAMS + SCORE h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 30, paddingLeft: 10, paddingRight: 10, }, teamColumn(c.homeTeam, accent, accentDark), scoreBlock, teamColumn(c.awayTeam, away, awayDark), ), // PICKS TITLE h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16, paddingLeft: 6, paddingRight: 6, }, h("div", { display: "flex", fontSize: 22, fontWeight: 900, color: "white", letterSpacing: 2 }, "EN İYİ 3 TAHMİN"), h("div", { display: "flex", alignItems: "center" }, h("div", { display: "flex", fontSize: 14, color: "rgba(255,255,255,0.55)", fontWeight: 700, marginRight: 8 }, "SKOR GÜVENİ"), h("div", { display: "flex", fontSize: 22, fontWeight: 900, color: accent }, `%${c.scoreConfidence}`), ), ), // PICKS LIST h("div", { display: "flex", flexDirection: "column" }, ...c.picks.slice(0, 3).map((p, i) => pickRow(p, i)), ), // FOOTER h("div", { display: "flex", marginTop: "auto", alignItems: "center", justifyContent: "space-between", paddingTop: 22, borderTop: "1px solid rgba(255,255,255,0.08)", }, h("div", { display: "flex", fontSize: 16, color: "rgba(255,255,255,0.45)", fontWeight: 600 }, "AI Destekli Maç Tahmini · iddaai"), h("div", { display: "flex", fontSize: 20, color: "white", fontWeight: 900, letterSpacing: 2 }, "iddaai.com"), ), ); } async function renderCard(c: Card, name: string) { const svg = await satori(buildCard(c) as any, { width: 1080, height: 1080, fonts: [ { name: "Arial", data: ARIAL, weight: 400, style: "normal" }, { name: "Arial", data: ARIAL_BOLD, weight: 700, style: "normal" }, { name: "Arial", data: ARIAL_BLACK, weight: 900, style: "normal" }, ], }); const png = new Resvg(svg).render().asPng(); const out = path.join(OUTPUT_DIR, `v2-${name}.png`); fs.writeFileSync(out, png); console.log(` → ${out}`); } async function main() { if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); await renderCard({ sport: "basketball", leagueName: "Şampiyonlar Ligi", countryName: "Avrupa", matchDate: "25 May 2026, 21:00", homeTeam: "Unicaja Malaga", awayTeam: "AEK", htScore: "40-35", ftScore: "88-75", scoreConfidence: 90, picks: [ { market: "Toplam Sayı Üst 160.5", pick: "Üst", confidence: 85, odds: 1.85 }, { market: "Ev Sahibi Toplam Üst 82.5", pick: "Üst", confidence: 83, odds: 1.78 }, { market: "Handikap Ev Sahibi -5.5", pick: "1", confidence: 78, odds: 1.92 }, ], }, "basket-unicaja-aek"); await renderCard({ sport: "football", leagueName: "Süper Lig", countryName: "Türkiye", matchDate: "26 May 2026, 20:00", homeTeam: "Galatasaray", awayTeam: "Fenerbahçe", htScore: "1-0", ftScore: "2-1", scoreConfidence: 72, picks: [ { market: "Maç Sonucu - Ev Sahibi", pick: "1", confidence: 68, odds: 2.15 }, { market: "Üst 2.5 Gol", pick: "Üst", confidence: 61, odds: 1.92 }, { market: "Karşılıklı Gol", pick: "Var", confidence: 58, odds: 1.75 }, ], }, "football-gs-fb"); } main().catch((e) => { console.error(e); process.exit(1); });