Compare commits
3 commits
036411e9d8
...
b05bad6554
| Author | SHA1 | Date | |
|---|---|---|---|
| b05bad6554 | |||
|
|
cab161424e | ||
|
|
3f16d5e1d3 |
6 changed files with 95 additions and 71 deletions
|
|
@ -14,7 +14,7 @@ const scoringRules = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("QualifyingPointsStandings", () => {
|
describe("QualifyingPointsStandings", () => {
|
||||||
it("renders fractional QP to hundredths", () => {
|
it("renders fractional QP to at most hundredths with trailing zeros trimmed", () => {
|
||||||
render(
|
render(
|
||||||
<QualifyingPointsStandings
|
<QualifyingPointsStandings
|
||||||
standings={[
|
standings={[
|
||||||
|
|
@ -52,7 +52,8 @@ describe("QualifyingPointsStandings", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("14.67 QP")).toBeInTheDocument();
|
expect(screen.getByText("14.67 QP")).toBeInTheDocument();
|
||||||
expect(screen.getByText("0.50 QP")).toBeInTheDocument();
|
// 0.50 trims its trailing zero to 0.5; 14.67 and 0.43 are unaffected.
|
||||||
|
expect(screen.getByText("0.5 QP")).toBeInTheDocument();
|
||||||
expect(screen.getByText("0.43 QP")).toBeInTheDocument();
|
expect(screen.getByText("0.43 QP")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ interface QualifyingPointsStandingsProps {
|
||||||
function formatQP(raw: string): string {
|
function formatQP(raw: string): string {
|
||||||
const n = parseFloat(raw);
|
const n = parseFloat(raw);
|
||||||
if (isNaN(n)) return "—";
|
if (isNaN(n)) return "—";
|
||||||
return n % 1 === 0 ? n.toString() : n.toFixed(2);
|
// At most 2 decimals, trailing zeros trimmed (1.50→1.5). Mirrors the Discord
|
||||||
|
// embed's formatQPValue (app/services/discord.ts) so the site and Discord agree.
|
||||||
|
return parseFloat(n.toFixed(2)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QualifyingPointsStandings({
|
export function QualifyingPointsStandings({
|
||||||
|
|
|
||||||
|
|
@ -747,17 +747,19 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
{ participantName: "Rafael Nadal", qpEarned: 0, qpTotal: 20, ownerUsername: "alex" },
|
{ participantName: "Rafael Nadal", qpEarned: 0, qpTotal: 20, ownerUsername: "alex" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
it("sends an embed with gold color and QP title", async () => {
|
it("sends an embed with gold color, QP title, no footer, and links the title to the standings page", async () => {
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
entries: BASE_ENTRIES,
|
entries: BASE_ENTRIES,
|
||||||
|
standingsUrl: "https://brackt.com/leagues/abc/sports-seasons/ss-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = getPayload();
|
const payload = getPayload();
|
||||||
expect(payload.embeds[0].title).toBe("🏅 Qualifying Points Update — Slam League 2025");
|
expect(payload.embeds[0].title).toBe("🏅 Qualifying Points Update — Slam League 2025");
|
||||||
expect(payload.embeds[0].color).toBe(0x5865f2);
|
expect(payload.embeds[0].color).toBe(0x5865f2);
|
||||||
expect(payload.embeds[0].footer).toEqual({ text: "brackt.com" });
|
expect(payload.embeds[0].url).toBe("https://brackt.com/leagues/abc/sports-seasons/ss-1");
|
||||||
|
expect(payload.embeds[0].footer).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows sport and event header", async () => {
|
it("shows sport and event header", async () => {
|
||||||
|
|
@ -782,13 +784,13 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("**Points Awarded**");
|
expect(desc).toContain("**Points Awarded**");
|
||||||
expect(desc).toContain("• **Novak Djokovic (alex)** — +20 QP");
|
expect(desc).toContain("• **Novak Djokovic (alex)** — 20 QP");
|
||||||
expect(desc).toContain("• **Carlos Alcaraz (chris)** — +14 QP");
|
expect(desc).toContain("• **Carlos Alcaraz (chris)** — 14 QP");
|
||||||
// Djokovic (20 QP) should appear before Alcaraz (14 QP)
|
// Djokovic (20 QP) should appear before Alcaraz (14 QP)
|
||||||
expect(desc.indexOf("Novak Djokovic")).toBeLessThan(desc.indexOf("Carlos Alcaraz"));
|
expect(desc.indexOf("Novak Djokovic")).toBeLessThan(desc.indexOf("Carlos Alcaraz"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows No Points section for entries with qpEarned == 0", async () => {
|
it("shows Non-scoring Participants section with season total and plain manager name", async () => {
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
|
|
@ -796,11 +798,12 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("**No Points**");
|
expect(desc).toContain("**Non-scoring Participants**");
|
||||||
expect(desc).toContain("Rafael Nadal (alex)");
|
// Name (seasonTotal, manager) — Rafael has a 20 QP running total, owner "alex"
|
||||||
|
expect(desc).toContain("• Rafael Nadal (20, alex)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows QP Standings for all entries sorted by qpTotal desc", async () => {
|
it("shows Drafted Participants in Top 8 for entries sorted by qpTotal desc", async () => {
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
|
|
@ -808,7 +811,7 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("**QP Standings**");
|
expect(desc).toContain("**Drafted Participants in Top 8**");
|
||||||
expect(desc).toContain("1\\. Novak Djokovic (alex) — 45 QP");
|
expect(desc).toContain("1\\. Novak Djokovic (alex) — 45 QP");
|
||||||
expect(desc).toContain("2\\. Carlos Alcaraz (chris) — 34 QP");
|
expect(desc).toContain("2\\. Carlos Alcaraz (chris) — 34 QP");
|
||||||
expect(desc).toContain("3\\. Rafael Nadal (alex) — 20 QP");
|
expect(desc).toContain("3\\. Rafael Nadal (alex) — 20 QP");
|
||||||
|
|
@ -854,17 +857,18 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
// Mirrors the web UI's formatQP: fractional QP renders with 2 decimals ("1.50"),
|
// Mirrors the web UI's formatQP: fractional QP renders with up to 2 decimals and
|
||||||
// never rounded to an integer.
|
// trailing zeros trimmed ("1.5"), never rounded to an integer. The Points Awarded
|
||||||
expect(desc).toContain("+1.50 QP");
|
// line no longer carries a "+".
|
||||||
expect(desc).not.toContain("+2 QP");
|
expect(desc).toContain("• **Novak Djokovic (snarkymcgee)** — 1.5 QP");
|
||||||
expect(desc).toContain("1.50 QP");
|
expect(desc).not.toContain("+");
|
||||||
expect(desc).not.toContain("— 2 QP");
|
expect(desc).not.toContain("2 QP");
|
||||||
|
expect(desc).not.toContain("1.50");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ranks the standings by caller-provided full-field globalRank, not position among entries", async () => {
|
it("excludes participants ranked outside the top 8 from the Drafted Participants section", async () => {
|
||||||
// Screenshot scenario: only two players scored this event, but they sit at T9 in
|
// Only the top 8 (plus ties) of the full season field belong in this section, so a
|
||||||
// the full season field (8 players ahead). They must show T9, not T1.
|
// rank-9 scorer must NOT appear in it — even though it still shows in Points Awarded.
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Rumble League 2026",
|
seasonName: "Rumble League 2026",
|
||||||
|
|
@ -872,28 +876,31 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
eventName: "Wimbledon",
|
eventName: "Wimbledon",
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
participantName: "Novak Djokovic",
|
participantName: "Player Eight",
|
||||||
qpEarned: 1.5,
|
qpEarned: 5,
|
||||||
qpTotal: 1.5,
|
qpTotal: 10,
|
||||||
globalRank: 9,
|
globalRank: 8,
|
||||||
globalRankTied: true,
|
globalRankTied: false,
|
||||||
ownerUsername: "snarkymcgee",
|
ownerUsername: "eighthowner",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
participantName: "Jannik Sinner",
|
participantName: "Player Nine",
|
||||||
qpEarned: 1.5,
|
qpEarned: 3,
|
||||||
qpTotal: 1.5,
|
qpTotal: 8,
|
||||||
globalRank: 9,
|
globalRank: 9,
|
||||||
globalRankTied: true,
|
globalRankTied: false,
|
||||||
ownerUsername: "tanay002",
|
ownerUsername: "ninthowner",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("T9\\. Novak Djokovic (snarkymcgee) — 1.50 QP");
|
expect(desc).toContain("**Drafted Participants in Top 8**");
|
||||||
expect(desc).toContain("T9\\. Jannik Sinner (tanay002) — 1.50 QP");
|
expect(desc).toContain("8\\. Player Eight (eighthowner) — 10 QP");
|
||||||
expect(desc).not.toContain("T1\\.");
|
// The rank-9 player is filtered out of the standings section entirely.
|
||||||
|
expect(desc).not.toContain("9\\. Player Nine");
|
||||||
|
// ...but both still earned points, so both remain in Points Awarded.
|
||||||
|
expect(desc).toContain("• **Player Nine (ninthowner)** — 3 QP");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Discord mention instead of username when ownerDiscordUserId provided", async () => {
|
it("uses Discord mention instead of username when ownerDiscordUserId provided", async () => {
|
||||||
|
|
@ -912,11 +919,13 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("Novak Djokovic (<@111222333>)");
|
// Points Awarded (a pinged section) uses the Discord mention...
|
||||||
expect(desc).not.toContain("(alex)");
|
expect(desc).toContain("• **Novak Djokovic (<@111222333>)** — 20 QP");
|
||||||
|
// ...while the (non-pinged) Top 8 standings section uses the plain username.
|
||||||
|
expect(desc).toContain("1\\. Novak Djokovic (alex) — 45 QP");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pings opted-in owners who appear in awarded or no-points sections", async () => {
|
it("pings awarded owners but not non-scoring owners", async () => {
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
|
|
@ -928,9 +937,9 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
|
|
||||||
const payload = getPayload();
|
const payload = getPayload();
|
||||||
expect(payload.content).toContain("<@111>");
|
expect(payload.content).toContain("<@111>");
|
||||||
expect(payload.content).toContain("<@222>");
|
expect(payload.content).not.toContain("<@222>");
|
||||||
expect(payload.allowed_mentions.users).toContain("111");
|
expect(payload.allowed_mentions.users).toContain("111");
|
||||||
expect(payload.allowed_mentions.users).toContain("222");
|
expect(payload.allowed_mentions.users).not.toContain("222");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not send when entries array is empty", async () => {
|
it("does not send when entries array is empty", async () => {
|
||||||
|
|
@ -956,7 +965,7 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
expect(desc).toContain("• Jakob Mensik (chris)");
|
expect(desc).toContain("• Jakob Mensik (chris)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends with only a Knocked Out section when there are no QP entries, omitting QP Standings", async () => {
|
it("sends with only a Knocked Out section when there are no QP entries, omitting the standings section", async () => {
|
||||||
await sendQualifyingPointsUpdateNotification({
|
await sendQualifyingPointsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
|
|
@ -967,7 +976,7 @@ describe("sendQualifyingPointsUpdateNotification", () => {
|
||||||
expect(fetch).toHaveBeenCalledOnce();
|
expect(fetch).toHaveBeenCalledOnce();
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("**Knocked Out**");
|
expect(desc).toContain("**Knocked Out**");
|
||||||
expect(desc).not.toContain("**QP Standings**");
|
expect(desc).not.toContain("**Drafted Participants in Top 8**");
|
||||||
expect(desc).not.toContain("**Points Awarded**");
|
expect(desc).not.toContain("**Points Awarded**");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { findDiscordIdsByUserIds } from "~/models/account";
|
||||||
const SPORTS_SEASON_ID = "ss-1";
|
const SPORTS_SEASON_ID = "ss-1";
|
||||||
const SCORING_EVENT_ID = "ev-1";
|
const SCORING_EVENT_ID = "ev-1";
|
||||||
const SEASON_ID = "season-1";
|
const SEASON_ID = "season-1";
|
||||||
|
const LEAGUE_ID = "league-1";
|
||||||
const PARTICIPANT_ID = "p-1";
|
const PARTICIPANT_ID = "p-1";
|
||||||
const OWNER_ID = "u-1";
|
const OWNER_ID = "u-1";
|
||||||
const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc";
|
const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc";
|
||||||
|
|
@ -68,7 +69,7 @@ function makeDb(overrides: Record<string, unknown> = {}) {
|
||||||
{
|
{
|
||||||
id: SEASON_ID,
|
id: SEASON_ID,
|
||||||
year: 2025,
|
year: 2025,
|
||||||
league: { name: "Slam League", discordWebhookUrl: WEBHOOK_URL },
|
league: { id: LEAGUE_ID, name: "Slam League", discordWebhookUrl: WEBHOOK_URL },
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
@ -176,6 +177,7 @@ describe("notifyQualifyingPointsUpdate", () => {
|
||||||
seasonName: "Slam League 2025",
|
seasonName: "Slam League 2025",
|
||||||
eventName: "Roland Garros 2025",
|
eventName: "Roland Garros 2025",
|
||||||
sportName: "Tennis",
|
sportName: "Tennis",
|
||||||
|
standingsUrl: `${process.env.APP_URL ?? "https://brackt.com"}/leagues/${LEAGUE_ID}/sports-seasons/${SPORTS_SEASON_ID}`,
|
||||||
entries: [
|
entries: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
participantName: "Carlos Alcaraz",
|
participantName: "Carlos Alcaraz",
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,12 @@ function escapeMarkdown(text: string): string {
|
||||||
/**
|
/**
|
||||||
* Format a QP value for display. QP is genuinely fractional (e.g. a tennis R16 loser
|
* Format a QP value for display. QP is genuinely fractional (e.g. a tennis R16 loser
|
||||||
* earns 1.5 QP from the 9–16 split), so we must NOT round: show whole numbers plainly
|
* earns 1.5 QP from the 9–16 split), so we must NOT round: show whole numbers plainly
|
||||||
* and fractional values to 2 decimals. Mirrors the web UI's formatQP
|
* and fractional values to at most 2 decimals with trailing zeros trimmed (1.50→1.5).
|
||||||
* (app/components/scoring/QualifyingPointsStandings.tsx) so Discord and the site agree.
|
* Mirrors the web UI's formatQP (app/components/scoring/QualifyingPointsStandings.tsx)
|
||||||
|
* so Discord and the site agree.
|
||||||
*/
|
*/
|
||||||
function formatQPValue(n: number): string {
|
function formatQPValue(n: number): string {
|
||||||
return n % 1 === 0 ? n.toString() : n.toFixed(2);
|
return parseFloat(n.toFixed(2)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StandingEntry {
|
export interface StandingEntry {
|
||||||
|
|
@ -294,6 +295,7 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
eventName,
|
eventName,
|
||||||
entries,
|
entries,
|
||||||
eliminated = [],
|
eliminated = [],
|
||||||
|
standingsUrl,
|
||||||
}: {
|
}: {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
seasonName: string;
|
seasonName: string;
|
||||||
|
|
@ -301,6 +303,7 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
entries: QPEventEntry[];
|
entries: QPEventEntry[];
|
||||||
eliminated?: QPEliminatedEntry[];
|
eliminated?: QPEliminatedEntry[];
|
||||||
|
standingsUrl?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (entries.length === 0 && eliminated.length === 0) return;
|
if (entries.length === 0 && eliminated.length === 0) return;
|
||||||
|
|
||||||
|
|
@ -326,23 +329,21 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
const label = managerLabel
|
const label = managerLabel
|
||||||
? `${escapeMarkdown(e.participantName)} (${managerLabel})`
|
? `${escapeMarkdown(e.participantName)} (${managerLabel})`
|
||||||
: escapeMarkdown(e.participantName);
|
: escapeMarkdown(e.participantName);
|
||||||
sections.push(`• **${label}** — +${formatQPValue(e.qpEarned)} QP`);
|
sections.push(`• **${label}** — ${formatQPValue(e.qpEarned)} QP`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const zeroEntries = entries.filter((e) => e.qpEarned === 0);
|
const zeroEntries = entries.filter((e) => e.qpEarned === 0);
|
||||||
if (zeroEntries.length > 0) {
|
if (zeroEntries.length > 0) {
|
||||||
sections.push("\n**No Points**");
|
sections.push("\n**Non-scoring Participants**");
|
||||||
for (const e of zeroEntries) {
|
for (const e of zeroEntries) {
|
||||||
const managerLabel = e.ownerDiscordUserId
|
// Plain manager username here (never a <@id> mention): this section is not
|
||||||
? `<@${e.ownerDiscordUserId}>`
|
// pinged, so mention chips would misleadingly look like a ping. Show the
|
||||||
: e.ownerUsername
|
// running season QP total alongside the manager, e.g. "Name (1.5, manager)".
|
||||||
? escapeMarkdown(e.ownerUsername)
|
const details = e.ownerUsername
|
||||||
: undefined;
|
? `${formatQPValue(e.qpTotal)}, ${escapeMarkdown(e.ownerUsername)}`
|
||||||
const label = managerLabel
|
: `${formatQPValue(e.qpTotal)}`;
|
||||||
? `${escapeMarkdown(e.participantName)} (${managerLabel})`
|
sections.push(`• ${escapeMarkdown(e.participantName)} (${details})`);
|
||||||
: escapeMarkdown(e.participantName);
|
|
||||||
sections.push(`• ${label}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,22 +364,22 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order by the participant's rank in the FULL season standings (globalRank),
|
// Only drafted participants sitting in the top 8 of the FULL season standings
|
||||||
// supplied by the caller, so the block reads as a season leaderboard slice.
|
// (globalRank supplied by the caller), ordered by rank. Standard competition
|
||||||
const sorted = [...entries].toSorted((a, b) => a.globalRank - b.globalRank);
|
// ranking collapses ties to a shared rank, so `globalRank <= 8` naturally keeps
|
||||||
|
// any tie group sitting at rank 8 ("top 8 plus ties").
|
||||||
|
const sorted = [...entries]
|
||||||
|
.filter((e) => e.globalRank <= 8)
|
||||||
|
.toSorted((a, b) => a.globalRank - b.globalRank);
|
||||||
|
|
||||||
// The standings block reflects QP earners; skip it entirely when this sync only
|
// Skip the section entirely when no drafted participant is in the top 8 (e.g. a
|
||||||
// reported knockouts (no QP change) so we don't emit an empty header.
|
// sync that only reported knockouts) so we don't emit an empty header.
|
||||||
if (sorted.length > 0) {
|
if (sorted.length > 0) {
|
||||||
sections.push("\n**QP Standings**");
|
sections.push("\n**Drafted Participants in Top 8**");
|
||||||
for (const e of sorted) {
|
for (const e of sorted) {
|
||||||
const rankPrefix = e.globalRankTied ? `T${e.globalRank}` : `${e.globalRank}`;
|
const rankPrefix = e.globalRankTied ? `T${e.globalRank}` : `${e.globalRank}`;
|
||||||
const ownerLabel = e.ownerDiscordUserId
|
// Plain manager username (no <@id> mention): this section is not pinged.
|
||||||
? `<@${e.ownerDiscordUserId}>`
|
const managerLabel = e.ownerUsername ? ` (${escapeMarkdown(e.ownerUsername)})` : "";
|
||||||
: e.ownerUsername
|
|
||||||
? escapeMarkdown(e.ownerUsername)
|
|
||||||
: undefined;
|
|
||||||
const managerLabel = ownerLabel ? ` (${ownerLabel})` : "";
|
|
||||||
sections.push(`${rankPrefix}\\. ${escapeMarkdown(e.participantName)}${managerLabel} — ${formatQPValue(e.qpTotal)} QP`);
|
sections.push(`${rankPrefix}\\. ${escapeMarkdown(e.participantName)}${managerLabel} — ${formatQPValue(e.qpTotal)} QP`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -389,8 +390,10 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
description = description.slice(0, MAX_DESCRIPTION - 3) + "...";
|
description = description.slice(0, MAX_DESCRIPTION - 3) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ping only managers who earned points or lost a drafted player this sync —
|
||||||
|
// never the non-scoring (zeroEntries) managers.
|
||||||
const pingUserIds = new Set<string>();
|
const pingUserIds = new Set<string>();
|
||||||
for (const e of [...awardedEntries, ...zeroEntries, ...eliminated]) {
|
for (const e of [...awardedEntries, ...eliminated]) {
|
||||||
if (e.ownerDiscordUserId) pingUserIds.add(e.ownerDiscordUserId);
|
if (e.ownerDiscordUserId) pingUserIds.add(e.ownerDiscordUserId);
|
||||||
}
|
}
|
||||||
const pingIds = [...pingUserIds];
|
const pingIds = [...pingUserIds];
|
||||||
|
|
@ -399,9 +402,9 @@ export async function sendQualifyingPointsUpdateNotification({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: `🏅 Qualifying Points Update — ${seasonName}`,
|
title: `🏅 Qualifying Points Update — ${seasonName}`,
|
||||||
|
url: standingsUrl,
|
||||||
description,
|
description,
|
||||||
color: 0x5865f2, // Discord blurple
|
color: 0x5865f2, // Discord blurple
|
||||||
footer: { text: "brackt.com" },
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,12 @@ export async function notifyQualifyingPointsUpdate(
|
||||||
|
|
||||||
const seasonName = `${league.name} ${season.year}`;
|
const seasonName = `${league.name} ${season.year}`;
|
||||||
|
|
||||||
|
// Link the embed title to this league's sport-season standings page. The
|
||||||
|
// sportsSeasonId is exactly what the /leagues/:leagueId/sports-seasons/:sportsSeasonId
|
||||||
|
// route expects (the page filters seasonSports by leagueId).
|
||||||
|
const appUrl = process.env.APP_URL ?? "https://brackt.com";
|
||||||
|
const standingsUrl = `${appUrl}/leagues/${league.id}/sports-seasons/${sportsSeasonId}`;
|
||||||
|
|
||||||
const picks = picksBySeasonId.get(seasonId) ?? [];
|
const picks = picksBySeasonId.get(seasonId) ?? [];
|
||||||
const teamByParticipantId = new Map<
|
const teamByParticipantId = new Map<
|
||||||
string,
|
string,
|
||||||
|
|
@ -209,6 +215,7 @@ export async function notifyQualifyingPointsUpdate(
|
||||||
eventName,
|
eventName,
|
||||||
entries,
|
entries,
|
||||||
eliminated,
|
eliminated,
|
||||||
|
standingsUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue