mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Show upcoming events on streams section
This commit is contained in:
parent
baa287fd78
commit
8d2f57f8c2
|
|
@ -92,7 +92,7 @@ export function ListLink({
|
|||
overlayIconUrl?: string;
|
||||
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
|
||||
subtitle?: React.ReactNode;
|
||||
badge?: string | null;
|
||||
badge?: React.ReactNode;
|
||||
badgeVariant?: "default" | "warning";
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -123,7 +123,7 @@ export function ListLink({
|
|||
{subtitle ? (
|
||||
<span className={styles.listLinkSubtitle}>{subtitle}</span>
|
||||
) : null}
|
||||
{badge ? (
|
||||
{typeof badge === "string" ? (
|
||||
<span
|
||||
className={clsx(styles.listLinkBadge, {
|
||||
[styles.listLinkBadgeWarning]: badgeVariant === "warning",
|
||||
|
|
@ -131,7 +131,9 @@ export function ListLink({
|
|||
>
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
) : (
|
||||
badge
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@
|
|||
height: 14px;
|
||||
}
|
||||
|
||||
.streamTierBadge {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
@ -130,6 +135,24 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.streamUpcomingDivider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
padding: var(--s-1) var(--s-2);
|
||||
font-size: var(--font-3xs);
|
||||
color: var(--color-text-high);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.streamUpcomingDivider::before,
|
||||
.streamUpcomingDivider::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.viewAllLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { Flipped, Flipper } from "react-flip-toolkit";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useFetcher, useLocation, useMatches } from "react-router";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { SidebarStream } from "~/features/core/streams/streams.server";
|
||||
import { FriendMenu } from "~/features/friends/components/FriendMenu";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { LanguageCode } from "~/modules/i18n/config";
|
||||
|
|
@ -35,6 +36,7 @@ import { TwitchIcon } from "../icons/Twitch";
|
|||
import { MobileNav } from "../MobileNav";
|
||||
import { ListLink, SideNav, SideNavFooter, SideNavHeader } from "../SideNav";
|
||||
import sideNavStyles from "../SideNav.module.css";
|
||||
import { TierPill } from "../TierPill";
|
||||
import { Footer } from "./Footer";
|
||||
import styles from "./index.module.css";
|
||||
import { LogInButtonContainer } from "./LogInButtonContainer";
|
||||
|
|
@ -255,44 +257,59 @@ export function Layout({
|
|||
{t("front:sideNav.noStreams")}
|
||||
</div>
|
||||
) : null}
|
||||
{streams.map((stream) => {
|
||||
{streams.map((stream, i) => {
|
||||
const startsAtDate = databaseTimestampToDate(stream.startsAt);
|
||||
const isUpcoming = startsAtDate.getTime() > Date.now();
|
||||
const prevStream = streams.at(i - 1);
|
||||
const prevIsLive =
|
||||
prevStream &&
|
||||
databaseTimestampToDate(prevStream.startsAt).getTime() <=
|
||||
Date.now();
|
||||
const showUpcomingDivider = isMounted && isUpcoming && prevIsLive;
|
||||
|
||||
return (
|
||||
<ListLink
|
||||
key={stream.id}
|
||||
to={stream.url}
|
||||
imageUrl={stream.imageUrl}
|
||||
overlayIconUrl={stream.overlayIconUrl}
|
||||
subtitle={
|
||||
stream.peakXp ? (
|
||||
<span className={styles.streamXpSubtitle}>
|
||||
<img
|
||||
src={`${navIconUrl("xsearch")}.png`}
|
||||
alt=""
|
||||
className={styles.streamXpIcon}
|
||||
/>
|
||||
{stream.peakXp}
|
||||
</span>
|
||||
) : stream.subtitle ? (
|
||||
stream.subtitle
|
||||
) : isMounted ? (
|
||||
formatDistanceToNow(startsAtDate, {
|
||||
addSuffix: true,
|
||||
language: i18n.language as LanguageCode,
|
||||
})
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
badge={
|
||||
isMounted && startsAtDate.getTime() < Date.now()
|
||||
? "LIVE"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{stream.name}
|
||||
</ListLink>
|
||||
<React.Fragment key={stream.id}>
|
||||
{showUpcomingDivider ? (
|
||||
<div className={styles.streamUpcomingDivider}>
|
||||
{t("front:sideNav.streams.upcoming")}
|
||||
</div>
|
||||
) : null}
|
||||
<ListLink
|
||||
to={stream.url}
|
||||
imageUrl={stream.imageUrl}
|
||||
overlayIconUrl={stream.overlayIconUrl}
|
||||
subtitle={
|
||||
stream.peakXp ? (
|
||||
<span className={styles.streamXpSubtitle}>
|
||||
<img
|
||||
src={`${navIconUrl("xsearch")}.png`}
|
||||
alt=""
|
||||
className={styles.streamXpIcon}
|
||||
/>
|
||||
{stream.peakXp}
|
||||
</span>
|
||||
) : stream.subtitle ? (
|
||||
stream.subtitle
|
||||
) : isMounted ? (
|
||||
isUpcoming ? (
|
||||
formatRelativeDate(stream.startsAt)
|
||||
) : (
|
||||
formatDistanceToNow(startsAtDate, {
|
||||
addSuffix: true,
|
||||
language: i18n.language as LanguageCode,
|
||||
})
|
||||
)
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
badge={
|
||||
isMounted && !isUpcoming ? "LIVE" : streamTierBadge(stream)
|
||||
}
|
||||
>
|
||||
{stream.name}
|
||||
</ListLink>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</SideNav>
|
||||
|
|
@ -483,3 +500,17 @@ function SideNavUserPanel() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function streamTierBadge(stream: SidebarStream): React.ReactNode {
|
||||
const tier = stream.tier ?? stream.tentativeTier;
|
||||
if (!tier) return undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.streamTierBadge}>
|
||||
<TierPill
|
||||
tier={tier}
|
||||
isTentative={!stream.tier && !!stream.tentativeTier}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function clearCombinedStreamsCache() {
|
|||
}
|
||||
|
||||
export type SidebarStream = {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
overlayIconUrl?: string;
|
||||
|
|
@ -21,6 +21,7 @@ export type SidebarStream = {
|
|||
subtitle: string;
|
||||
startsAt: number;
|
||||
tier: TournamentTierNumber | null;
|
||||
tentativeTier?: number;
|
||||
peakXp?: number;
|
||||
twitchUsername?: string;
|
||||
};
|
||||
|
|
@ -30,7 +31,7 @@ export function getLiveTournamentStreams(): SidebarStream[] {
|
|||
|
||||
for (const tournament of RunningTournaments.all) {
|
||||
streams.push({
|
||||
id: tournament.ctx.id,
|
||||
id: `tournament-${tournament.ctx.id}`,
|
||||
name: tournament.ctx.name,
|
||||
imageUrl: tournament.ctx.logoUrl,
|
||||
url: tournamentStreamsPage(tournament.ctx.id),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ interface ParticipationInfo {
|
|||
organizers: Set<ShowcaseCalendarEvent["id"]>;
|
||||
}
|
||||
|
||||
export async function upcomingTournaments(): Promise<ShowcaseCalendarEvent[]> {
|
||||
const tournaments = await cachedTournaments();
|
||||
return tournaments.upcoming;
|
||||
}
|
||||
|
||||
export async function frontPageTournamentsByUserId(
|
||||
userId: number | null,
|
||||
): Promise<ShowcaseTournamentCollection> {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export async function getSendouQSidebarStreams(): Promise<
|
|||
|
||||
entries.push({
|
||||
sidebarStream: {
|
||||
id: -matchId,
|
||||
id: `sendouq-${matchId}`,
|
||||
name: `Match #${matchId}`,
|
||||
imageUrl: averageTier
|
||||
? `${tierImageUrl(averageTier.name)}.png`
|
||||
|
|
|
|||
|
|
@ -8,10 +8,21 @@ export function rank(
|
|||
streams: RankedStream[],
|
||||
maxStreams: number,
|
||||
): SidebarStream[] {
|
||||
return streams
|
||||
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)
|
||||
.map((rs) => rs.stream);
|
||||
.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);
|
||||
}
|
||||
|
||||
export function tournamentTierToScore(
|
||||
|
|
@ -20,6 +31,10 @@ export function tournamentTierToScore(
|
|||
return tier ?? 9;
|
||||
}
|
||||
|
||||
export function upcomingTournamentTierToScore(tier: number): number {
|
||||
return Math.min(9, tier + 4);
|
||||
}
|
||||
|
||||
export function sendouQTierToScore(tier: {
|
||||
name: TierName;
|
||||
isPlus: boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { cachified } from "@epic-web/cachified";
|
||||
import { addDays } from "date-fns";
|
||||
import { href } from "react-router";
|
||||
import * as R from "remeda";
|
||||
import type { ShowcaseCalendarEvent } from "~/features/calendar/calendar-types";
|
||||
|
|
@ -13,7 +14,9 @@ import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournam
|
|||
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
import { getSendouQSidebarStreams } from "~/features/sendouq-streams/core/streams.server";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
import { cache, ttl } from "~/utils/cache.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import {
|
||||
BLANK_IMAGE_URL,
|
||||
discordAvatarUrl,
|
||||
|
|
@ -47,6 +50,7 @@ export type SidebarFriend = {
|
|||
const MAX_EVENTS_VISIBLE = 5;
|
||||
const MAX_FRIENDS_VISIBLE = 4;
|
||||
const MAX_STREAMS_VISIBLE = 5;
|
||||
const UPCOMING_TOURNAMENT_WINDOW_DAYS = 3;
|
||||
const SENDOUQ_QUOTA = 2;
|
||||
const TOURNAMENT_SUB_QUOTA = 2;
|
||||
|
||||
|
|
@ -130,9 +134,10 @@ function combinedStreamsCached(): Promise<SidebarStream[]> {
|
|||
|
||||
async function combinedStreams(): Promise<SidebarStream[]> {
|
||||
const tournamentStreams = getLiveTournamentStreams();
|
||||
const [sendouQEntries, xRankRows] = await Promise.all([
|
||||
const [sendouQEntries, xRankRows, upcomingTournaments] = await Promise.all([
|
||||
getSendouQSidebarStreams(),
|
||||
LiveStreamRepository.findXRankStreams(),
|
||||
ShowcaseTournaments.upcomingTournaments(),
|
||||
]);
|
||||
|
||||
const seenUsernames = new Set(
|
||||
|
|
@ -176,7 +181,7 @@ async function combinedStreams(): Promise<SidebarStream[]> {
|
|||
|
||||
ranked.push({
|
||||
stream: {
|
||||
id: row.id,
|
||||
id: `xrank-${row.id}`,
|
||||
name: row.username,
|
||||
imageUrl: row.discordAvatar
|
||||
? discordAvatarUrl({
|
||||
|
|
@ -197,6 +202,30 @@ async function combinedStreams(): Promise<SidebarStream[]> {
|
|||
});
|
||||
}
|
||||
|
||||
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.startTime < nowTimestamp) continue;
|
||||
if (event.startTime > threeDaysFromNow) continue;
|
||||
if (event.hidden) continue;
|
||||
|
||||
ranked.push({
|
||||
stream: {
|
||||
id: `upcoming-${event.id}`,
|
||||
name: event.name,
|
||||
imageUrl: event.logoUrl ?? BLANK_IMAGE_URL,
|
||||
url: event.url,
|
||||
subtitle: "",
|
||||
startsAt: event.startTime,
|
||||
tier: (event.tier as TournamentTierNumber) ?? null,
|
||||
tentativeTier: event.tentativeTier ?? undefined,
|
||||
},
|
||||
score: StreamRanking.upcomingTournamentTierToScore(effectiveTier),
|
||||
});
|
||||
}
|
||||
|
||||
return StreamRanking.rank(ranked, MAX_STREAMS_VISIBLE);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "Log in to follow your friends' activity",
|
||||
"sideNav.friends.noFriends": "Add friends to see their activity here",
|
||||
"sideNav.streams": "Streams",
|
||||
"sideNav.streams.upcoming": "Upcoming",
|
||||
"sideNav.scrimVs": "vs. {{opponent}}",
|
||||
"sideNav.lookingForScrim": "Looking for scrim",
|
||||
"mobileNav.menu": "Menu",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"sideNav.friends.notLoggedIn": "",
|
||||
"sideNav.friends.noFriends": "",
|
||||
"sideNav.streams": "",
|
||||
"sideNav.streams.upcoming": "",
|
||||
"sideNav.scrimVs": "",
|
||||
"sideNav.lookingForScrim": "",
|
||||
"mobileNav.menu": "",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user