/**
* 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); });