mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-26 21:27:48 -05:00
@@ -25,7 +25,9 @@ import {
|
||||
concatUserSubmittedImagePrefix,
|
||||
jsonArrayFrom,
|
||||
jsonObjectFrom,
|
||||
tournamentCheckedInTeams,
|
||||
tournamentLogoWithDefault,
|
||||
tournamentMembersCount,
|
||||
} from "~/utils/kysely.server";
|
||||
import { calendarEventPage, tournamentPage } from "~/utils/urls";
|
||||
import {
|
||||
@@ -141,26 +143,9 @@ const withOrganization = (eb: ExpressionBuilder<DB, "CalendarEvent">) =>
|
||||
const withTeamsCount = (
|
||||
eb: ExpressionBuilder<DB, "CalendarEventDate" | "Tournament">,
|
||||
) =>
|
||||
eb
|
||||
.selectFrom("TournamentTeam")
|
||||
.leftJoin("TournamentTeamCheckIn", (join) =>
|
||||
join
|
||||
.on("TournamentTeamCheckIn.bracketIdx", "is", null)
|
||||
.onRef(
|
||||
"TournamentTeamCheckIn.tournamentTeamId",
|
||||
"=",
|
||||
"TournamentTeam.id",
|
||||
),
|
||||
)
|
||||
.whereRef("TournamentTeam.tournamentId", "=", "Tournament.id")
|
||||
.where("TournamentTeam.isPlaceholder", "=", 0)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("TournamentTeamCheckIn.checkedInAt", "is not", null),
|
||||
eb("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
|
||||
]),
|
||||
)
|
||||
.select(({ fn }) => [fn.countAll<number>().as("teamsCount")]);
|
||||
tournamentCheckedInTeams(eb).select(({ fn }) => [
|
||||
fn.countAll<number>().as("count"),
|
||||
]);
|
||||
|
||||
function findAllBetweenTwoTimestampsQuery({
|
||||
startTime,
|
||||
@@ -191,6 +176,7 @@ function findAllBetweenTwoTimestampsQuery({
|
||||
),
|
||||
withOrganization(eb).as("organization"),
|
||||
withTeamsCount(eb).as("teamsCount"),
|
||||
tournamentMembersCount(eb).as("membersCount"),
|
||||
tournamentLogoWithDefault(eb).as("logoUrl"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -224,7 +210,7 @@ function findAllBetweenTwoTimestampsQuery({
|
||||
dateToDatabaseTimestamp(startTime),
|
||||
)
|
||||
.where("CalendarEventDate.startsAt", "<=", dateToDatabaseTimestamp(endTime))
|
||||
.$narrowType<{ teamsCount: NotNull }>()
|
||||
.$narrowType<{ teamsCount: NotNull; membersCount: NotNull }>()
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -260,6 +246,8 @@ function findAllBetweenTwoTimestampsMapped(
|
||||
authorId: row.authorId,
|
||||
tags: tags.filter((tag) => !EXCLUDED_TAGS.includes(tag)),
|
||||
teamsCount: row.teamsCount,
|
||||
membersCount: row.membersCount,
|
||||
minMembersPerTeam: row.tournamentSettings?.minMembersPerTeam ?? 4,
|
||||
normalizedTeamCount: normalizedTeamCount({
|
||||
teamsCount: row.teamsCount,
|
||||
minMembersPerTeam: row.tournamentSettings?.minMembersPerTeam ?? 4,
|
||||
|
||||
@@ -11,6 +11,9 @@ interface CommonEvent {
|
||||
id: number;
|
||||
name: string;
|
||||
teamsCount: number;
|
||||
/** How many players in total are rostered in the counted teams */
|
||||
membersCount: number;
|
||||
minMembersPerTeam: number;
|
||||
logoUrl: string | null;
|
||||
url: string;
|
||||
/** Is the tournament ranked? If null, tournament is not hosted on sendou.ink */
|
||||
@@ -51,7 +54,6 @@ export interface ShowcaseCalendarEvent extends CommonEvent {
|
||||
/** Tournament is hidden from the public (test tournament) */
|
||||
hidden: boolean;
|
||||
isFinalized: boolean;
|
||||
minMembersPerTeam: number;
|
||||
firstPlacers: Array<{
|
||||
teamName: string;
|
||||
logoUrl: string | null;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
max-width: 165px;
|
||||
}
|
||||
|
||||
.teamCount {
|
||||
.participantsPill {
|
||||
font-size: var(--font-2xs);
|
||||
font-weight: var(--weight-bold);
|
||||
background-color: var(--color-bg-higher);
|
||||
@@ -92,6 +92,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-1);
|
||||
white-space: nowrap;
|
||||
|
||||
& svg {
|
||||
width: var(--tournament-card-icon-size);
|
||||
@@ -99,6 +100,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.participantsTeamsCount {
|
||||
color: var(--color-text-high);
|
||||
font-weight: var(--weight-semi);
|
||||
}
|
||||
|
||||
.modesPillContainer {
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Trophy } from "~/features/trophies/components/Trophy";
|
||||
import { useFormatDistanceToNow } from "~/hooks/intl/useFormatDistanceToNow";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSpoilerFree } from "~/hooks/useSpoilerFree";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { navIconUrl } from "~/utils/urls";
|
||||
import type { CalendarEvent, ShowcaseCalendarEvent } from "../calendar-types";
|
||||
@@ -35,6 +36,10 @@ export function TournamentCard({
|
||||
const isShowcase = tournament.type === "showcase";
|
||||
const isCalendar = tournament.type === "calendar";
|
||||
const isHostedOnSendouInk = typeof tournament.isRanked === "boolean";
|
||||
const modes =
|
||||
tournament.modes && !isDefaultModes(tournament.modes)
|
||||
? tournament.modes
|
||||
: null;
|
||||
|
||||
const startDate = isShowcase
|
||||
? databaseTimestampToDate(tournament.startsAt)
|
||||
@@ -135,10 +140,10 @@ export function TournamentCard({
|
||||
{isShowcase && "hasVods" in tournament && tournament.hasVods ? (
|
||||
<div className={styles.vodIndicator}>📺 VODs</div>
|
||||
) : null}
|
||||
{tournament.modes ? <ModesPill modes={tournament.modes} /> : null}
|
||||
{modes ? <ModesPill modes={modes} /> : null}
|
||||
<div
|
||||
className={clsx(styles.pillsContainer, {
|
||||
[styles.lonely]: !tournament.modes && isHostedOnSendouInk,
|
||||
[styles.lonely]: !modes && isHostedOnSendouInk,
|
||||
})}
|
||||
>
|
||||
{tournament.isRanked ? (
|
||||
@@ -155,9 +160,11 @@ export function TournamentCard({
|
||||
/>
|
||||
) : null}
|
||||
{isHostedOnSendouInk ? (
|
||||
<div className={styles.teamCount}>
|
||||
<Users /> {tournament.teamsCount}
|
||||
</div>
|
||||
<ParticipantsPill
|
||||
teamsCount={tournament.teamsCount}
|
||||
membersCount={tournament.membersCount}
|
||||
minMembersPerTeam={tournament.minMembersPerTeam}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,6 +291,30 @@ function SpoilerRevealPill({ onReveal }: { onReveal: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantsPill({
|
||||
teamsCount,
|
||||
membersCount,
|
||||
minMembersPerTeam,
|
||||
}: {
|
||||
teamsCount: number;
|
||||
membersCount: number;
|
||||
minMembersPerTeam: number;
|
||||
}) {
|
||||
const isSolo = minMembersPerTeam === 1;
|
||||
|
||||
return (
|
||||
<div className={styles.participantsPill}>
|
||||
<Users />
|
||||
<span>
|
||||
{isSolo ? teamsCount : membersCount}
|
||||
{isSolo ? null : (
|
||||
<span className={styles.participantsTeamsCount}>/{teamsCount}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModesPill({ modes }: { modes: NonNullable<CalendarEvent["modes"]> }) {
|
||||
const size = 16;
|
||||
|
||||
@@ -334,3 +365,11 @@ function PrizesPill({
|
||||
</SendouPopover>
|
||||
);
|
||||
}
|
||||
|
||||
/** Modes pill is only interesting when the tournament deviates from the standard ranked modes */
|
||||
function isDefaultModes(modes: NonNullable<CalendarEvent["modes"]>) {
|
||||
return (
|
||||
modes.length === rankedModesShort.length &&
|
||||
rankedModesShort.every((mode) => modes.includes(mode))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ function makeEvent(
|
||||
tags: [],
|
||||
modes: ["SZ"],
|
||||
teamsCount: 2,
|
||||
membersCount: 8,
|
||||
minMembersPerTeam: 4,
|
||||
organization: null,
|
||||
authorId: 1,
|
||||
type: "calendar",
|
||||
|
||||
@@ -304,6 +304,7 @@ function mapTournamentFromDB(
|
||||
name: tournament.name,
|
||||
startsAt: tournament.startsAt,
|
||||
teamsCount: tournament.teamsCount,
|
||||
membersCount: tournament.membersCount,
|
||||
logoUrl: tournament.logoUrl,
|
||||
organization: tournament.organization
|
||||
? {
|
||||
|
||||
@@ -26,7 +26,9 @@ import {
|
||||
concatUserSubmittedImagePrefix,
|
||||
jsonArrayFrom,
|
||||
jsonObjectFrom,
|
||||
tournamentCheckedInTeams,
|
||||
tournamentLogoWithDefault,
|
||||
tournamentMembersCount,
|
||||
tournamentUsername,
|
||||
} from "~/utils/kysely.server";
|
||||
import type { Unwrapped } from "~/utils/types";
|
||||
@@ -798,29 +800,12 @@ export function findAllForShowcase() {
|
||||
"CalendarEvent.organizationId",
|
||||
"CalendarEventDate.startsAt",
|
||||
"CalendarEvent.hidden",
|
||||
eb
|
||||
.selectFrom("TournamentTeam")
|
||||
.leftJoin("TournamentTeamCheckIn", (join) =>
|
||||
join
|
||||
.on("TournamentTeamCheckIn.bracketIdx", "is", null)
|
||||
.onRef(
|
||||
"TournamentTeamCheckIn.tournamentTeamId",
|
||||
"=",
|
||||
"TournamentTeam.id",
|
||||
),
|
||||
)
|
||||
.whereRef("TournamentTeam.tournamentId", "=", "Tournament.id")
|
||||
.where("TournamentTeam.isPlaceholder", "=", 0)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("TournamentTeamCheckIn.checkedInAt", "is not", null),
|
||||
eb("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
|
||||
]),
|
||||
)
|
||||
tournamentCheckedInTeams(eb)
|
||||
.select(({ fn }) => [
|
||||
fn.count<number>("TournamentTeam.id").distinct().as("teamsCount"),
|
||||
fn.count<number>("TournamentTeam.id").distinct().as("count"),
|
||||
])
|
||||
.as("teamsCount"),
|
||||
tournamentMembersCount(eb).as("membersCount"),
|
||||
tournamentLogoWithDefault(eb).as("logoUrl"),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
@@ -888,7 +873,7 @@ export function findAllForShowcase() {
|
||||
])
|
||||
.where("CalendarEventDate.startsAt", ">", databaseTimestampWeekAgo())
|
||||
.orderBy("CalendarEventDate.startsAt", "asc")
|
||||
.$narrowType<{ teamsCount: NotNull }>()
|
||||
.$narrowType<{ teamsCount: NotNull; membersCount: NotNull }>()
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "~/db/json-selections";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import { databaseTimestampNow } from "./dates";
|
||||
import { IS_E2E_TEST_RUN } from "./e2e";
|
||||
import { safeNumberParse } from "./number";
|
||||
|
||||
@@ -289,6 +290,73 @@ export function tournamentTeamCount(
|
||||
.where("TournamentTeam.isPlaceholder", "=", 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subquery resolving to a tournament's non-placeholder teams that are either checked in to the
|
||||
* tournament itself (not to a specific bracket) or belong to a tournament that has not started yet.
|
||||
* Correlates on `"Tournament"."id"` and `"CalendarEventDate"."startsAt"`. Has no select of its own,
|
||||
* so extend it with the aggregate the caller needs, e.g. `.select(({ fn }) => fn.countAll().as("count"))`.
|
||||
*/
|
||||
export function tournamentCheckedInTeams(
|
||||
eb: ExpressionBuilder<DB, "CalendarEventDate" | "Tournament">,
|
||||
) {
|
||||
return eb
|
||||
.selectFrom("TournamentTeam")
|
||||
.leftJoin("TournamentTeamCheckIn", (join) =>
|
||||
join
|
||||
.on("TournamentTeamCheckIn.bracketIdx", "is", null)
|
||||
.onRef(
|
||||
"TournamentTeamCheckIn.tournamentTeamId",
|
||||
"=",
|
||||
"TournamentTeam.id",
|
||||
),
|
||||
)
|
||||
.whereRef("TournamentTeam.tournamentId", "=", "Tournament.id")
|
||||
.where("TournamentTeam.isPlaceholder", "=", 0)
|
||||
.where((eb2) =>
|
||||
eb2.or([
|
||||
eb2("TournamentTeamCheckIn.checkedInAt", "is not", null),
|
||||
eb2("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expression resolving to a tournament's participant count: rostered players of the teams from
|
||||
* {@link tournamentCheckedInTeams} while the tournament is still to come, players who actually got
|
||||
* a result once it has been finalized. Correlates on `"Tournament"."id"`. Alias it
|
||||
* `.as("membersCount")` when selecting it directly.
|
||||
*/
|
||||
export function tournamentMembersCount(
|
||||
eb: ExpressionBuilder<DB, "CalendarEventDate" | "Tournament">,
|
||||
) {
|
||||
return eb
|
||||
.case()
|
||||
.when("Tournament.isFinalized", "=", 1)
|
||||
.then(
|
||||
eb
|
||||
.selectFrom("TournamentResult")
|
||||
.whereRef("TournamentResult.tournamentId", "=", "Tournament.id")
|
||||
.select(({ fn }) => [
|
||||
fn.count<number>("TournamentResult.userId").distinct().as("count"),
|
||||
]),
|
||||
)
|
||||
.else(
|
||||
tournamentCheckedInTeams(eb)
|
||||
.innerJoin(
|
||||
"TournamentTeamMember",
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
"TournamentTeam.id",
|
||||
)
|
||||
.select(({ fn }) => [
|
||||
fn
|
||||
.count<number>("TournamentTeamMember.userId")
|
||||
.distinct()
|
||||
.as("count"),
|
||||
]),
|
||||
)
|
||||
.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped subquery picking each user's (`by: "userId"`) or team's (`by: "identifier"`) latest
|
||||
* Skill row of a season: `latestId` plus that row's `ordinal`, `matchesCount` and the `by`
|
||||
|
||||
Reference in New Issue
Block a user