sendou.ink/app/features/sidebar/routes/sidebar.ts
Kalle 2d7bc0ffc0 Add scrims to sidebar Events section
Display user's scrims alongside tournaments in the sidebar's Events section:
- LFG scrims show "Scrim" with scrims icon and "Looking" badge
- Booked scrims show "vs. {opponent}" with opponent avatar and "Booked" badge
- Tournaments deduplicated (no duplicates when both organizing and participating)
- Events sorted chronologically and limited to 8

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 20:57:20 +02:00

212 lines
5.0 KiB
TypeScript

import { href, type LoaderFunctionArgs } from "react-router";
import { getUser } from "~/features/auth/core/user.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
import { RunningTournaments } from "~/features/tournament-bracket/core/RunningTournaments.server";
import {
navIconUrl,
sendouQMatchPage,
tournamentBracketsPage,
tournamentMatchPage,
} from "~/utils/urls";
export type SidebarEvent = {
id: number;
name: string;
url: string;
logoUrl: string | null;
startTime: number;
type: "tournament" | "scrim";
scrimStatus?: "booked" | "looking";
};
const MAX_EVENTS_VISIBLE = 8;
export const loader = async (_args: LoaderFunctionArgs) => {
const user = getUser();
if (!user) {
return {
events: [] as SidebarEvent[],
matchStatus: null as { matchId: number; url: string } | null,
tournamentMatchStatus: null as {
url: string;
text: string;
roundName?: string;
logoUrl: string | null;
} | null,
friends: getMockFriends(),
streams: getMockStreams(),
};
}
const [tournamentsData, scrimsData, ownGroup] = await Promise.all([
ShowcaseTournaments.frontPageTournamentsByUserId(user.id),
ScrimPostRepository.findUserScrims(user.id),
Promise.resolve(SendouQ.findOwnGroup(user.id)),
]);
const tournamentMatchStatus = resolveTournamentMatchStatus(user.id);
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((t) => ({
id: t.id,
name: t.name,
url: t.url,
logoUrl: t.logoUrl,
startTime: t.startTime,
type: "tournament" as const,
}));
const scrimsIconUrl = `${navIconUrl("scrims")}.png`;
const scrimEvents: SidebarEvent[] = scrimsData.map((s) => ({
id: s.id,
name: s.opponentName ?? "Scrim",
url: s.isAccepted
? href("/scrims/:id", { id: String(s.id) })
: href("/scrims"),
logoUrl: s.opponentAvatarUrl ?? scrimsIconUrl,
startTime: s.at,
type: "scrim" as const,
scrimStatus: s.isAccepted ? ("booked" as const) : ("looking" as const),
}));
const events = [...tournamentEvents, ...scrimEvents]
.sort((a, b) => a.startTime - b.startTime)
.slice(0, MAX_EVENTS_VISIBLE);
return {
events,
matchStatus: ownGroup?.matchId
? { matchId: ownGroup.matchId, url: sendouQMatchPage(ownGroup.matchId) }
: null,
tournamentMatchStatus,
friends: getMockFriends(),
streams: getMockStreams(),
};
};
function resolveTournamentMatchStatus(userId: number) {
const tournament = RunningTournaments.getUserTournament(userId);
if (!tournament) return null;
const status = tournament.teamMemberOfProgressStatus({ id: userId });
if (!status) return null;
const tournamentId = tournament.ctx.id;
const logoUrl = tournament.ctx.logoUrl;
if (status.type === "MATCH") {
const roundInfo = tournament.matchContextNamesById(status.matchId);
const roundNameWithoutNumbers = (roundInfo?.roundName ?? "Match").replace(
/\.\d+$/,
"",
);
return {
url: tournamentMatchPage({ tournamentId, matchId: status.matchId }),
text: "MATCH",
roundName: roundNameWithoutNumbers,
logoUrl,
};
}
if (status.type === "CHECKIN") {
return {
url: tournamentBracketsPage({ tournamentId }),
text: "CHECKIN",
logoUrl,
};
}
if (
status.type === "WAITING_FOR_MATCH" ||
status.type === "WAITING_FOR_CAST" ||
status.type === "WAITING_FOR_ROUND" ||
status.type === "WAITING_FOR_BRACKET"
) {
return {
url: tournamentBracketsPage({ tournamentId }),
text: "WAITING",
logoUrl,
};
}
return null;
}
function getMockFriends() {
return [
{
id: 1,
name: "Splat_Master",
avatarUrl: "https://i.pravatar.cc/150?u=friend1",
subtitle: "SendouQ",
badge: "2/4",
},
{
id: 2,
name: "InklingPro",
avatarUrl: "https://i.pravatar.cc/150?u=friend2",
subtitle: "Lobby",
badge: "2/8",
},
{
id: 3,
name: "OctoKing",
avatarUrl: "https://i.pravatar.cc/150?u=friend3",
subtitle: "In The Zone 22",
badge: "3/4",
},
{
id: 4,
name: "TurfWarrior",
avatarUrl: "https://i.pravatar.cc/150?u=friend4",
subtitle: "SendouQ",
badge: "1/4",
},
{
id: 5,
name: "RankedGrinder",
avatarUrl: "https://i.pravatar.cc/150?u=friend5",
subtitle: "Lobby",
badge: "5/8",
},
];
}
function getMockStreams() {
return [
{
id: 3,
name: "Paddling Pool 252",
imageUrl: "https://i.pravatar.cc/150?u=stream1",
subtitle: "Losers Finals",
badge: "LIVE",
},
{
id: 1,
name: "Splash Go!",
imageUrl:
"https://liquipedia.net/commons/images/7/73/Splash_Go_allmode.png",
subtitle: "Tomorrow, 9:00 AM",
},
{
id: 2,
name: "Area Cup",
imageUrl:
"https://pbs.twimg.com/profile_images/1830601967821017088/4SDZVKdj_400x400.jpg",
subtitle: "Saturday, 10 AM",
},
];
}