Dashboard Studio 2.2.1: sports match centre, Import JSON back, no double entities
- Sports card: crests in team colours, position and record, countdown, win-probability bar, live clock and last play, venue - Import JSON back in the builder's menu - Fixed: picking an entity with the keyboard added it twice; existing duplicates are removed when dashboards load Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1547,18 +1547,48 @@
|
||||
if (!s) return missing(c.entity, "a TeamTracker sensor");
|
||||
const a = s.attributes, state = String(s.state).toUpperCase();
|
||||
if (state === "NOT_FOUND" || state === "BYE" || !a.opponent_name) return `<div class="sp"><div class="sp-league">${esc(a.league || friendly(s))}</div><div class="empty">${state === "BYE" ? t("No match this round.") : t("No match planned.")}</div></div>`;
|
||||
const team = { name: a.team_name, short: a.team_abbr, logo: a.team_logo, score: a.team_score, win: a.team_winner },
|
||||
opp = { name: a.opponent_name, short: a.opponent_abbr, logo: a.opponent_logo, score: a.opponent_score, win: a.opponent_winner };
|
||||
// TeamTracker gives each side its colours, record, league position and, often, its chance to win.
|
||||
const hues = (v) => (Array.isArray(v) ? v : String(v || "").split(",")).map((x) => String(x).trim()).map((x) => (x.startsWith("#") ? x : `#${x}`)).filter((x) => /^#[0-9a-f]{6}$/i.test(x));
|
||||
const side = (p) => ({ name: a[`${p}_name`], short: a[`${p}_abbr`], logo: a[`${p}_logo`], score: a[`${p}_score`], win: a[`${p}_winner`],
|
||||
colors: hues(a[`${p}_colors`]), record: a[`${p}_record`], rank: a[`${p}_rank`], prob: Number(a[`${p}_win_probability`]) });
|
||||
const team = side("team"), opp = side("opponent");
|
||||
const [home, away] = a.team_homeaway === "away" ? [opp, team] : [team, opp];
|
||||
const side = (x) => `<div class="sp-team ${x.win ? "win" : ""}">${x.logo ? `<img src="${esc(x.logo)}" alt="">` : `<span class="sp-badge">${esc((x.short || x.name || "?").slice(0, 3))}</span>`}<span>${esc(x.name || x.short || "")}</span></div>`;
|
||||
const light = (hex) => { const n = parseInt(hex.slice(1), 16); return 0.299 * (n >> 16) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255) > 160; };
|
||||
const crest = (x) => {
|
||||
if (x.logo) return `<img class="sp-logo" src="${esc(x.logo)}" alt="">`;
|
||||
const c1 = x.colors[0] || "#8e8e93", c2 = x.colors[1] && x.colors[1].toLowerCase() !== c1.toLowerCase() ? x.colors[1] : light(c1) ? "#1c1c1e" : "#ffffff";
|
||||
return `<svg class="sp-crest" viewBox="0 0 40 46" aria-hidden="true"><path d="M20 2 L37 8 V22 C37 33 29 41 20 44 C11 41 3 33 3 22 V8 Z" fill="${c1}" stroke="${c2}" stroke-width="2.5"/>
|
||||
<text x="20" y="27" text-anchor="middle" font-size="10.5" font-weight="800" fill="${light(c1) ? "#1c1c1e" : "#ffffff"}">${esc((x.short || x.name || "?").slice(0, 3).toUpperCase())}</text></svg>`;
|
||||
};
|
||||
const meta = (x) => [x.rank ? `#${x.rank}` : "", x.record].filter(Boolean).join(" · ");
|
||||
const sideHTML = (x) => `<div class="sp-side ${x.win ? "win" : ""}">${crest(x)}<span class="sp-name">${esc(x.name || x.short || "")}</span>${meta(x) ? `<span class="sp-meta num">${esc(meta(x))}</span>` : ""}</div>`;
|
||||
const kick = a.date ? new Date(a.date) : null;
|
||||
let mid;
|
||||
if (state === "PRE") mid = `<span class="sp-time num">${kick ? esc(kick.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })) : "–"}</span><span class="sp-sub">${esc(kick ? dayLabel(kick) : "")}${a.kickoff_in ? ` · ${esc(a.kickoff_in)}` : ""}</span>`;
|
||||
else mid = `<span class="sp-score num">${esc(home.score ?? 0)}<i>–</i>${esc(away.score ?? 0)}</span><span class="sp-sub">${state === "IN" ? `<b class="sp-live">${t("Live")}</b> ${esc(a.clock || "")}` : t("Full time")}</span>`;
|
||||
const extra = c.venue !== false ? [a.venue, a.tv_network].filter(Boolean).join(" · ") : "";
|
||||
return `<div class="sp ${state === "IN" ? "live" : ""}"><div class="sp-league">${esc(a.league || a.sport || "")}${a.event_name && state !== "PRE" ? "" : ""}</div>
|
||||
<div class="sp-match">${side(home)}<div class="sp-mid">${mid}</div>${side(away)}</div>
|
||||
${extra ? `<div class="sp-extra">${esc(extra)}</div>` : ""}${state === "IN" && a.last_play ? `<div class="sp-extra">${esc(a.last_play)}</div>` : ""}</div>`;
|
||||
if (state === "PRE") {
|
||||
let until = "";
|
||||
if (kick && kick > Date.now()) {
|
||||
const m = Math.round((kick - Date.now()) / 60000), d = Math.floor(m / 1440);
|
||||
until = d ? t("in {0} d {1} h", d, Math.floor((m % 1440) / 60)) : m >= 60 ? t("in {0} h {1} min", Math.floor(m / 60), m % 60) : t("in {0} min", m);
|
||||
}
|
||||
mid = `<span class="sp-time num">${kick ? esc(kick.toLocaleTimeString(LOCALE, { hour: "2-digit", minute: "2-digit" })) : "–"}</span>
|
||||
<span class="sp-sub">${esc(kick ? dayLabel(kick) : "")}</span>${until ? `<span class="sp-until">${esc(until)}</span>` : ""}`;
|
||||
} else mid = `<span class="sp-score num">${esc(home.score ?? 0)}<i>–</i>${esc(away.score ?? 0)}</span>
|
||||
<span class="sp-sub">${state === "IN" ? `<b class="sp-live">${t("Live")}</b> <span class="num">${esc(a.clock || "")}</span>` : t("Full time")}</span>`;
|
||||
// The chance of each result, as one bar in the teams' colours.
|
||||
let odds = "";
|
||||
if (isFinite(home.prob) && isFinite(away.prob) && home.prob + away.prob > 0) {
|
||||
const draw = Math.max(0, 1 - home.prob - away.prob), pc = (v) => Math.round(v * 100);
|
||||
const seg = (v, col, cls) => (v > 0.005 ? `<i class="${cls}" style="flex-basis:${(v * 100).toFixed(1)}%;${col ? `background:${col}` : ""}"></i>` : "");
|
||||
odds = `<div class="sp-odds" aria-label="${esc(t("Chance to win: {0} {1}%, draw {2}%, {3} {4}%", home.name, pc(home.prob), pc(draw), away.name, pc(away.prob)))}">
|
||||
<div class="sp-bar">${seg(home.prob, home.colors[0], "h")}${draw > 0.02 ? seg(draw, "", "d") : ""}${seg(away.prob, away.colors[0], "a")}</div>
|
||||
<div class="sp-odds-l num"><span>${pc(home.prob)}%</span>${draw > 0.02 ? `<span>${esc(t("Draw"))} ${pc(draw)}%</span>` : ""}<span>${pc(away.prob)}%</span></div></div>`;
|
||||
}
|
||||
const place = c.venue !== false ? [a.venue, a.location].filter(Boolean).join(", ") : "";
|
||||
return `<div class="sp ${state === "IN" ? "live" : ""} ${state === "POST" ? "done" : ""}">
|
||||
<div class="sp-head"><span class="sp-league">${esc([a.league || a.sport, a.event_name && a.event_name !== a.league ? a.event_name : ""].filter(Boolean).join(" · "))}</span>${c.venue !== false && a.tv_network ? `<span class="sp-tv">${esc(a.tv_network)}</span>` : ""}</div>
|
||||
<div class="sp-match">${sideHTML(home)}<div class="sp-mid">${mid}</div>${sideHTML(away)}</div>
|
||||
${odds}
|
||||
${state === "IN" && a.last_play ? `<div class="sp-play">${esc(a.last_play)}</div>` : place ? `<div class="sp-place">${Icons.svg("pin")}<span>${esc(place)}</span></div>` : ""}</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user