mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import * as StreamRanking from "./StreamRanking";
|
||||
|
||||
describe("StreamRanking.sendouQTierToScore", () => {
|
||||
test("LEVIATHAN+ scores 1", () => {
|
||||
expect(
|
||||
StreamRanking.sendouQTierToScore({ name: "LEVIATHAN", isPlus: true }),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test("PLATINUM+ scores 5", () => {
|
||||
expect(
|
||||
StreamRanking.sendouQTierToScore({ name: "PLATINUM", isPlus: true }),
|
||||
).toBe(5);
|
||||
});
|
||||
|
||||
test("IRON & SILVER+ scores 9 (capped)", () => {
|
||||
expect(
|
||||
StreamRanking.sendouQTierToScore({ name: "SILVER", isPlus: true }),
|
||||
).toBe(9);
|
||||
|
||||
expect(
|
||||
StreamRanking.sendouQTierToScore({ name: "IRON", isPlus: false }),
|
||||
).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StreamRanking.xpToScore", () => {
|
||||
test("returns null for XP below 3000", () => {
|
||||
expect(StreamRanking.xpToScore(2999)).toBeNull();
|
||||
expect(StreamRanking.xpToScore(0)).toBeNull();
|
||||
});
|
||||
|
||||
test("3000 XP scores 9", () => {
|
||||
expect(StreamRanking.xpToScore(3000)).toBe(9);
|
||||
});
|
||||
|
||||
test("3200 XP scores 8", () => {
|
||||
expect(StreamRanking.xpToScore(3200)).toBe(8);
|
||||
});
|
||||
|
||||
test("3400 XP scores 7", () => {
|
||||
expect(StreamRanking.xpToScore(3400)).toBe(7);
|
||||
});
|
||||
|
||||
test("3800 XP scores 5 (X rank minimum)", () => {
|
||||
expect(StreamRanking.xpToScore(3800)).toBe(5);
|
||||
});
|
||||
|
||||
test("XP above 3800 is capped at score 5", () => {
|
||||
expect(StreamRanking.xpToScore(4200)).toBe(5);
|
||||
expect(StreamRanking.xpToScore(4600)).toBe(5);
|
||||
expect(StreamRanking.xpToScore(9999)).toBe(5);
|
||||
});
|
||||
});
|
||||
82
apps/web-react/app/features/sidebar/core/StreamRanking.ts
Normal file
82
apps/web-react/app/features/sidebar/core/StreamRanking.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { SidebarStream } from "~/features/core/streams/streams.server";
|
||||
import { TIERS, type TierName } from "~/features/mmr/mmr-constants";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
|
||||
type RankedStream = { stream: SidebarStream; score: number };
|
||||
|
||||
/**
|
||||
* Score for admin-curated external streams. Below every other source's minimum (0) so they sort
|
||||
* to the top of the ranking, reserving the first sidebar slots.
|
||||
*/
|
||||
export const EXTERNAL_STREAM_SCORE = -1;
|
||||
|
||||
export function rank(
|
||||
streams: RankedStream[],
|
||||
maxStreams: number,
|
||||
): SidebarStream[] {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const selected = streams
|
||||
.sort((a, b) => a.score - b.score || a.stream.startsAt - b.stream.startsAt)
|
||||
.slice(0, maxStreams);
|
||||
|
||||
const live = selected
|
||||
.filter((rs) => rs.stream.startsAt <= now)
|
||||
.sort((a, b) => a.score - b.score);
|
||||
|
||||
const upcoming = selected
|
||||
.filter((rs) => rs.stream.startsAt > now)
|
||||
.sort((a, b) => a.stream.startsAt - b.stream.startsAt);
|
||||
|
||||
return [...live, ...upcoming].map((rs) => rs.stream);
|
||||
}
|
||||
|
||||
const SMALL_TOURNAMENT_PENALTY = 4;
|
||||
|
||||
export function tournamentTierToScore(
|
||||
tier: TournamentTierNumber | null,
|
||||
membersPerTeam?: number,
|
||||
): number {
|
||||
const base = tier ?? 9;
|
||||
const isSmallTeamSize = (membersPerTeam ?? 4) < 4;
|
||||
|
||||
return Math.min(9, base + (isSmallTeamSize ? SMALL_TOURNAMENT_PENALTY : 0));
|
||||
}
|
||||
|
||||
export function upcomingTournamentTierToScore(
|
||||
tier: number,
|
||||
membersPerTeam?: number,
|
||||
): number {
|
||||
const isSmallTeamSize = (membersPerTeam ?? 4) < 4;
|
||||
|
||||
return Math.min(
|
||||
9,
|
||||
tier + 4 + (isSmallTeamSize ? SMALL_TOURNAMENT_PENALTY : 0),
|
||||
);
|
||||
}
|
||||
|
||||
export function sendouQTierToScore(tier: {
|
||||
name: TierName;
|
||||
isPlus: boolean;
|
||||
}): number {
|
||||
const baseIndex = TIERS.findIndex((t) => t.name === tier.name);
|
||||
if (baseIndex === -1) return 9;
|
||||
return Math.min(9, baseIndex * 2 + (tier.isPlus ? 1 : 2));
|
||||
}
|
||||
|
||||
const X_RANK_SCORES = [
|
||||
[3800, 5],
|
||||
[3600, 6],
|
||||
[3400, 7],
|
||||
[3200, 8],
|
||||
[3000, 9],
|
||||
] as const;
|
||||
|
||||
export function minXpForStreamToBeShown(): number {
|
||||
return X_RANK_SCORES.at(-1)?.[0] ?? 3_000;
|
||||
}
|
||||
|
||||
export function xpToScore(peakXp: number): number | null {
|
||||
const entry = X_RANK_SCORES.find(([minXp]) => peakXp >= minXp);
|
||||
return entry ? entry[1] : null;
|
||||
}
|
||||
439
apps/web-react/app/features/sidebar/core/sidebar.server.ts
Normal file
439
apps/web-react/app/features/sidebar/core/sidebar.server.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
import { cachified } from "@epic-web/cachified";
|
||||
import { addDays } from "date-fns";
|
||||
import { href } from "react-router";
|
||||
import * as R from "remeda";
|
||||
import * as ExternalStreamRepository from "~/features/admin/ExternalStreamRepository.server";
|
||||
import { userIsBanned } from "~/features/ban/core/banned.server";
|
||||
import type { ShowcaseCalendarEvent } from "~/features/calendar/calendar-types";
|
||||
import {
|
||||
COMBINED_STREAMS_KEY,
|
||||
getLiveTournamentStreamerTwitchNames,
|
||||
getLiveTournamentStreams,
|
||||
type SidebarStream,
|
||||
} from "~/features/core/streams/streams.server";
|
||||
import * as FriendRepository from "~/features/friends/FriendRepository.server";
|
||||
import {
|
||||
type FriendActivityType,
|
||||
isInProgressFriendActivity,
|
||||
} from "~/features/friends/friends-constants";
|
||||
import {
|
||||
type FriendActivity,
|
||||
resolveFriendActivity,
|
||||
resolveSendouQMatchStreams,
|
||||
} from "~/features/friends/friends-utils.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
|
||||
import type { SidebarScrim } from "~/features/scrims/ScrimPostRepository.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
import { scrimsSearchParams } from "~/features/scrims/scrims-search-params";
|
||||
import { getSendouQSidebarStreams } from "~/features/sendouq-streams/core/streams.server";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
import * as SavedCalendarEventRepository from "~/features/tournament/SavedCalendarEventRepository.server";
|
||||
import { cache, ttl } from "~/utils/cache.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import type { CommonUser } from "~/utils/kysely.server";
|
||||
import {
|
||||
BLANK_IMAGE_URL,
|
||||
discordAvatarUrl,
|
||||
navIconUrl,
|
||||
twitchUrl,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import * as StreamRanking from "./StreamRanking";
|
||||
|
||||
export type SidebarEvent = {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
logoUrl: string | null;
|
||||
/** Whose avatar the event shows instead of a logo of its own. */
|
||||
user: CommonUser | null;
|
||||
startsAt: number;
|
||||
type: "tournament" | "scrim";
|
||||
scrimStatus?: "booked" | "looking" | "requestPending";
|
||||
};
|
||||
|
||||
export type SidebarFriend = {
|
||||
id: number;
|
||||
name: string;
|
||||
discordId: string;
|
||||
discordAvatar: string | null;
|
||||
customAvatarUrl: string | null;
|
||||
url: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
activityType: FriendActivityType | null;
|
||||
matchId: number | null;
|
||||
tournamentId: number | null;
|
||||
streamUrl: string | null;
|
||||
};
|
||||
|
||||
const MAX_EVENTS_VISIBLE = 5;
|
||||
const MAX_FRIENDS_VISIBLE = 8;
|
||||
const MAX_STREAMS_VISIBLE = 5;
|
||||
const UPCOMING_TOURNAMENT_WINDOW_DAYS = 3;
|
||||
const SENDOUQ_QUOTA = 2;
|
||||
const TOURNAMENT_SUB_QUOTA = 2;
|
||||
|
||||
export async function resolveSidebarData(userId: number | null) {
|
||||
if (!userId) {
|
||||
return {
|
||||
events: [] as SidebarEvent[],
|
||||
friends: [] as SidebarFriend[],
|
||||
streams: await combinedStreamsCached(),
|
||||
savedTournamentIds: [] as number[],
|
||||
incomingFriendRequestIds: [] as number[],
|
||||
};
|
||||
}
|
||||
|
||||
const [
|
||||
tournamentsData,
|
||||
scrimsData,
|
||||
friendsWithActivity,
|
||||
savedTournaments,
|
||||
incomingFriendRequestIds,
|
||||
streamedSendouQMatches,
|
||||
] = await Promise.all([
|
||||
ShowcaseTournaments.categorizedTournamentsByUserId(userId),
|
||||
ScrimPostRepository.findUserScrims(userId),
|
||||
FriendRepository.findByUserIdWithActivity(userId),
|
||||
SavedCalendarEventRepository.findAllUpcomingByUserId(userId),
|
||||
FriendRepository.findPendingReceivedRequestIds(userId),
|
||||
resolveSendouQMatchStreams(),
|
||||
]);
|
||||
|
||||
const seenTournamentIds = new Set<number>();
|
||||
const tournamentEvents: SidebarEvent[] = [
|
||||
...tournamentsData.participatingFor,
|
||||
...tournamentsData.organizingFor,
|
||||
]
|
||||
.filter((t) => {
|
||||
if (seenTournamentIds.has(t.id)) return false;
|
||||
seenTournamentIds.add(t.id);
|
||||
return true;
|
||||
})
|
||||
.map(tournamentToSidebarEvent);
|
||||
|
||||
const savedEvents: SidebarEvent[] = savedTournaments
|
||||
.filter((t) => !seenTournamentIds.has(t.id))
|
||||
.map((t) => {
|
||||
seenTournamentIds.add(t.id);
|
||||
return tournamentToSidebarEvent(t);
|
||||
});
|
||||
|
||||
const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);
|
||||
|
||||
const events = [...tournamentEvents, ...savedEvents, ...scrimEvents]
|
||||
.sort((a, b) => a.startsAt - b.startsAt)
|
||||
.slice(0, MAX_EVENTS_VISIBLE);
|
||||
|
||||
const friends = resolveFriends(friendsWithActivity, streamedSendouQMatches);
|
||||
|
||||
const savedTournamentIds = savedTournaments.map((t) => t.id);
|
||||
|
||||
return {
|
||||
events,
|
||||
friends,
|
||||
streams: await combinedStreamsCached(),
|
||||
savedTournamentIds,
|
||||
incomingFriendRequestIds,
|
||||
};
|
||||
}
|
||||
|
||||
function combinedStreamsCached(): Promise<SidebarStream[]> {
|
||||
return cachified({
|
||||
key: COMBINED_STREAMS_KEY,
|
||||
cache,
|
||||
ttl: ttl(10 * 60 * 1000),
|
||||
async getFreshValue() {
|
||||
return combinedStreams();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function combinedStreams(): Promise<SidebarStream[]> {
|
||||
const tournamentStreams = getLiveTournamentStreams();
|
||||
const [sendouQEntries, xRankRows, upcomingTournaments, externalStreams] =
|
||||
await Promise.all([
|
||||
getSendouQSidebarStreams(),
|
||||
LiveStreamRepository.findXRankStreams(),
|
||||
ShowcaseTournaments.upcomingTournaments(),
|
||||
ExternalStreamRepository.findAllForSidebar(),
|
||||
]);
|
||||
|
||||
const seenUsernames = new Set([
|
||||
...getLiveTournamentStreamerTwitchNames(),
|
||||
...sendouQEntries.flatMap((e) =>
|
||||
e.twitchUsernames.map((t) => t.toLowerCase()),
|
||||
),
|
||||
]);
|
||||
|
||||
const ranked: { stream: SidebarStream; score: number }[] = [];
|
||||
|
||||
for (const externalStream of externalStreams) {
|
||||
ranked.push({
|
||||
stream: {
|
||||
id: `external-${externalStream.id}`,
|
||||
name: externalStream.name,
|
||||
imageUrl: externalStream.avatarUrl ?? BLANK_IMAGE_URL,
|
||||
url: externalStream.url,
|
||||
subtitle: "",
|
||||
startsAt: externalStream.startsAt,
|
||||
tier: null,
|
||||
},
|
||||
score: StreamRanking.EXTERNAL_STREAM_SCORE,
|
||||
});
|
||||
}
|
||||
|
||||
for (const stream of tournamentStreams) {
|
||||
ranked.push({
|
||||
stream,
|
||||
score: StreamRanking.tournamentTierToScore(
|
||||
stream.tier,
|
||||
stream.membersPerTeam,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
for (const { sidebarStream, tier } of sendouQEntries) {
|
||||
const score = tier ? StreamRanking.sendouQTierToScore(tier) : 9;
|
||||
ranked.push({ stream: sidebarStream, score });
|
||||
}
|
||||
|
||||
const xRankByUser = new Map<number, (typeof xRankRows)[number]>();
|
||||
for (const row of xRankRows) {
|
||||
const existing = xRankByUser.get(row.id);
|
||||
if (!existing || (row.peakXp ?? 0) > (existing.peakXp ?? 0)) {
|
||||
xRankByUser.set(row.id, row);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of xRankByUser.values()) {
|
||||
if (userIsBanned(row.id)) continue;
|
||||
|
||||
if (
|
||||
row.twitchUsername &&
|
||||
seenUsernames.has(row.twitchUsername.toLowerCase())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const score = StreamRanking.xpToScore(row.peakXp ?? 0);
|
||||
if (score === null) continue;
|
||||
|
||||
ranked.push({
|
||||
stream: {
|
||||
id: `xrank-${row.id}`,
|
||||
name: row.username,
|
||||
imageUrl: row.customAvatarUrl
|
||||
? row.customAvatarUrl
|
||||
: row.discordAvatar
|
||||
? discordAvatarUrl({
|
||||
discordId: row.discordId,
|
||||
discordAvatar: row.discordAvatar,
|
||||
size: "sm",
|
||||
})
|
||||
: BLANK_IMAGE_URL,
|
||||
url: row.twitchUsername
|
||||
? twitchUrl(row.twitchUsername)
|
||||
: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
|
||||
subtitle: "",
|
||||
startsAt: Math.floor(Date.now() / 1000),
|
||||
tier: null,
|
||||
peakXp: row.peakXp ?? undefined,
|
||||
},
|
||||
score,
|
||||
});
|
||||
}
|
||||
|
||||
const nowTimestamp = dateToDatabaseTimestamp(new Date());
|
||||
const threeDaysFromNow = dateToDatabaseTimestamp(
|
||||
addDays(new Date(), UPCOMING_TOURNAMENT_WINDOW_DAYS),
|
||||
);
|
||||
for (const event of upcomingTournaments) {
|
||||
const effectiveTier = event.tier ?? event.tentativeTier;
|
||||
if (effectiveTier === null) continue;
|
||||
if (event.startsAt < nowTimestamp) continue;
|
||||
if (event.startsAt > threeDaysFromNow) continue;
|
||||
if (event.hidden) continue;
|
||||
|
||||
const membersPerTeam = event.minMembersPerTeam ?? 4;
|
||||
|
||||
ranked.push({
|
||||
stream: {
|
||||
id: `upcoming-${event.id}`,
|
||||
name: event.name,
|
||||
imageUrl: event.logoUrl ?? BLANK_IMAGE_URL,
|
||||
url: event.url,
|
||||
subtitle: "",
|
||||
startsAt: event.startsAt,
|
||||
tier: (event.tier as TournamentTierNumber) ?? null,
|
||||
membersPerTeam,
|
||||
tentativeTier: event.tentativeTier ?? undefined,
|
||||
},
|
||||
score: StreamRanking.upcomingTournamentTierToScore(
|
||||
effectiveTier,
|
||||
membersPerTeam,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return StreamRanking.rank(ranked, MAX_STREAMS_VISIBLE);
|
||||
}
|
||||
|
||||
type FriendWithActivity = Awaited<
|
||||
ReturnType<typeof FriendRepository.findByUserIdWithActivity>
|
||||
>[number];
|
||||
|
||||
function resolveFriends(
|
||||
friendsWithActivity: FriendWithActivity[],
|
||||
streamedSendouQMatches: ReadonlyMap<number, string>,
|
||||
) {
|
||||
const activityForRow = (row: FriendWithActivity) =>
|
||||
resolveFriendActivity({
|
||||
friendId: row.id,
|
||||
tournamentId: row.tournamentId,
|
||||
tournamentName: row.tournamentName,
|
||||
teamMemberCount: row.teamMemberCount,
|
||||
tournamentMinTeamSize: row.tournamentMinTeamSize,
|
||||
sendouQMatchStreams: streamedSendouQMatches,
|
||||
});
|
||||
|
||||
const unique = R.uniqueBy(friendsWithActivity, (f) => f.id);
|
||||
const friendRows = unique.filter((f) => f.friendshipId !== null);
|
||||
const teamMemberRows = unique.filter((f) => f.friendshipId === null);
|
||||
|
||||
const activeFriends: SidebarFriend[] = [];
|
||||
const sendouqFriends: SidebarFriend[] = [];
|
||||
const tournamentSubFriends: SidebarFriend[] = [];
|
||||
const inactiveFriends: FriendWithActivity[] = [];
|
||||
|
||||
for (const friend of friendRows) {
|
||||
const activity = activityForRow(friend);
|
||||
|
||||
if (!activity.type) {
|
||||
inactiveFriends.push(friend);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sidebarFriend = rowToSidebarFriend(friend, activity);
|
||||
|
||||
if (isInProgressFriendActivity(activity.type)) {
|
||||
activeFriends.push(sidebarFriend);
|
||||
} else if (activity.type === "SENDOUQ") {
|
||||
sendouqFriends.push(sidebarFriend);
|
||||
} else {
|
||||
tournamentSubFriends.push(sidebarFriend);
|
||||
}
|
||||
}
|
||||
|
||||
const result: SidebarFriend[] = [];
|
||||
|
||||
const sendouqToShow = sendouqFriends.slice(0, SENDOUQ_QUOTA);
|
||||
const tournamentToShow = tournamentSubFriends.slice(0, TOURNAMENT_SUB_QUOTA);
|
||||
|
||||
result.push(...sendouqToShow, ...tournamentToShow);
|
||||
|
||||
const remaining = MAX_FRIENDS_VISIBLE - result.length;
|
||||
if (remaining > 0) {
|
||||
const extraSendouq = sendouqFriends.slice(SENDOUQ_QUOTA);
|
||||
const extraTournament = tournamentSubFriends.slice(TOURNAMENT_SUB_QUOTA);
|
||||
result.push(...[...extraSendouq, ...extraTournament].slice(0, remaining));
|
||||
}
|
||||
|
||||
if (result.length < MAX_FRIENDS_VISIBLE) {
|
||||
result.push(...activeFriends.slice(0, MAX_FRIENDS_VISIBLE - result.length));
|
||||
}
|
||||
|
||||
if (result.length < MAX_FRIENDS_VISIBLE) {
|
||||
const shownIds = new Set(result.map((f) => f.id));
|
||||
const inactiveTeamMembers: FriendWithActivity[] = [];
|
||||
|
||||
for (const tm of teamMemberRows) {
|
||||
if (result.length >= MAX_FRIENDS_VISIBLE) break;
|
||||
if (shownIds.has(tm.id)) continue;
|
||||
|
||||
const activity = activityForRow(tm);
|
||||
if (!activity.type) {
|
||||
inactiveTeamMembers.push(tm);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(rowToSidebarFriend(tm, activity));
|
||||
shownIds.add(tm.id);
|
||||
}
|
||||
|
||||
for (const friend of inactiveFriends) {
|
||||
if (result.length >= MAX_FRIENDS_VISIBLE) break;
|
||||
if (shownIds.has(friend.id)) continue;
|
||||
|
||||
result.push(rowToSidebarFriend(friend, null));
|
||||
shownIds.add(friend.id);
|
||||
}
|
||||
|
||||
for (const tm of inactiveTeamMembers) {
|
||||
if (result.length >= MAX_FRIENDS_VISIBLE) break;
|
||||
|
||||
result.push(rowToSidebarFriend(tm, null));
|
||||
}
|
||||
}
|
||||
|
||||
return result.slice(0, MAX_FRIENDS_VISIBLE);
|
||||
}
|
||||
|
||||
function rowToSidebarFriend(
|
||||
row: FriendWithActivity,
|
||||
activity: FriendActivity | null,
|
||||
): SidebarFriend {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.username,
|
||||
discordId: row.discordId,
|
||||
discordAvatar: row.discordAvatar,
|
||||
customAvatarUrl: row.customAvatarUrl,
|
||||
url: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
|
||||
subtitle: activity?.subtitle ?? "",
|
||||
badge: activity?.badge ?? "",
|
||||
activityType: activity?.type ?? null,
|
||||
matchId: activity?.matchId ?? null,
|
||||
tournamentId: activity?.tournamentId ?? row.tournamentId,
|
||||
streamUrl: activity?.streamUrl ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function tournamentToSidebarEvent(
|
||||
t: ShowcaseCalendarEvent,
|
||||
): SidebarEvent {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
url: t.url,
|
||||
logoUrl: t.logoUrl,
|
||||
user: null,
|
||||
startsAt: t.startsAt,
|
||||
type: "tournament" as const,
|
||||
};
|
||||
}
|
||||
|
||||
const SCRIMS_ICON_URL = `${navIconUrl("scrims")}.avif`;
|
||||
|
||||
export function scrimToSidebarEvent(s: SidebarScrim): SidebarEvent {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.opponentName ?? "Scrim",
|
||||
url:
|
||||
s.status === "booked"
|
||||
? href("/scrims/:id", { id: String(s.id) })
|
||||
: s.status === "requestPending"
|
||||
? scrimsSearchParams.href(href("/scrims"), {
|
||||
pendingRequestPostId: s.id,
|
||||
})
|
||||
: href("/scrims"),
|
||||
// an opponent without a team is shown by their owner's avatar instead
|
||||
logoUrl: s.opponentAvatarUrl ?? (s.opponentUser ? null : SCRIMS_ICON_URL),
|
||||
user: s.opponentUser,
|
||||
startsAt: s.startsAt,
|
||||
type: "scrim" as const,
|
||||
scrimStatus: s.status,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user