Tournament showcase on the front page

This commit is contained in:
Kalle
2024-02-25 15:16:28 +02:00
parent bc99395843
commit 9cdd602737
8 changed files with 307 additions and 90 deletions

View File

@@ -1,4 +1,49 @@
[
{
"name": "sendouq",
"url": "q",
"prefetch": false,
"filters": [
"invert(65%) sepia(88%) saturate(376%) hue-rotate(80deg) brightness(97%) contrast(84%)",
"invert(63%) sepia(46%) saturate(1700%) hue-rotate(5deg) brightness(102%) contrast(103%)"
]
},
{
"name": "analyzer",
"url": "analyzer",
"prefetch": true,
"filters": [
"invert(27%) sepia(69%) saturate(5653%) hue-rotate(220deg) brightness(96%) contrast(85%)",
"invert(23%) sepia(83%) saturate(2378%) hue-rotate(334deg) brightness(87%) contrast(102%)"
]
},
{
"name": "builds",
"url": "builds",
"prefetch": true,
"filters": [
"invert(16%) sepia(80%) saturate(5634%) hue-rotate(272deg) brightness(91%) contrast(105%)",
"invert(71%) sepia(42%) saturate(4219%) hue-rotate(150deg) brightness(99%) contrast(86%)"
]
},
{
"name": "object-damage-calculator",
"url": "object-damage-calculator",
"prefetch": true,
"filters": [
"invert(19%) sepia(97%) saturate(6605%) hue-rotate(326deg) brightness(103%) contrast(105%)",
"invert(20%) sepia(99%) saturate(4341%) hue-rotate(220deg) brightness(102%) contrast(104%)"
]
},
{
"name": "leaderboards",
"url": "leaderboards",
"prefetch": false,
"filters": [
"invert(37%) sepia(60%) saturate(2835%) hue-rotate(349deg) brightness(100%) contrast(98%)",
"invert(26%) sepia(43%) saturate(7146%) hue-rotate(314deg) brightness(100%) contrast(98%)"
]
},
{
"name": "plans",
"url": "plans",
@@ -8,15 +53,6 @@
"invert(68%) sepia(78%) saturate(364%) hue-rotate(74deg) brightness(100%) contrast(100%)"
]
},
{
"name": "maps",
"url": "maps",
"prefetch": false,
"filters": [
"invert(53%) sepia(87%) saturate(411%) hue-rotate(42deg) brightness(95%) contrast(90%)",
"invert(81%) sepia(25%) saturate(0%) hue-rotate(166deg) brightness(89%) contrast(82%)"
]
},
{
"name": "badges",
"url": "badges",
@@ -53,42 +89,6 @@
"invert(53%) sepia(0%) saturate(1071%) hue-rotate(253deg) brightness(95%) contrast(84%)"
]
},
{
"name": "object-damage-calculator",
"url": "object-damage-calculator",
"prefetch": true,
"filters": [
"invert(19%) sepia(97%) saturate(6605%) hue-rotate(326deg) brightness(103%) contrast(105%)",
"invert(20%) sepia(99%) saturate(4341%) hue-rotate(220deg) brightness(102%) contrast(104%)"
]
},
{
"name": "analyzer",
"url": "analyzer",
"prefetch": true,
"filters": [
"invert(27%) sepia(69%) saturate(5653%) hue-rotate(220deg) brightness(96%) contrast(85%)",
"invert(23%) sepia(83%) saturate(2378%) hue-rotate(334deg) brightness(87%) contrast(102%)"
]
},
{
"name": "builds",
"url": "builds",
"prefetch": true,
"filters": [
"invert(16%) sepia(80%) saturate(5634%) hue-rotate(272deg) brightness(91%) contrast(105%)",
"invert(71%) sepia(42%) saturate(4219%) hue-rotate(150deg) brightness(99%) contrast(86%)"
]
},
{
"name": "sendouq",
"url": "q",
"prefetch": false,
"filters": [
"invert(65%) sepia(88%) saturate(376%) hue-rotate(80deg) brightness(97%) contrast(84%)",
"invert(63%) sepia(46%) saturate(1700%) hue-rotate(5deg) brightness(102%) contrast(103%)"
]
},
{
"name": "xsearch",
"url": "xsearch",
@@ -98,15 +98,6 @@
"invert(99%) sepia(1%) saturate(1035%) hue-rotate(293deg) brightness(100%) contrast(98%)"
]
},
{
"name": "leaderboards",
"url": "leaderboards",
"prefetch": false,
"filters": [
"invert(37%) sepia(60%) saturate(2835%) hue-rotate(349deg) brightness(100%) contrast(98%)",
"invert(26%) sepia(43%) saturate(7146%) hue-rotate(314deg) brightness(100%) contrast(98%)"
]
},
{
"name": "articles",
"url": "a",
@@ -151,5 +142,14 @@
"invert(76%) sepia(19%) saturate(6678%) hue-rotate(55deg) brightness(106%) contrast(122%)",
"invert(99%) sepia(57%) saturate(1676%) hue-rotate(333deg) brightness(86%) contrast(79%)"
]
},
{
"name": "maps",
"url": "maps",
"prefetch": false,
"filters": [
"invert(53%) sepia(87%) saturate(411%) hue-rotate(42deg) brightness(95%) contrast(90%)",
"invert(81%) sepia(25%) saturate(0%) hue-rotate(166deg) brightness(89%) contrast(82%)"
]
}
]

View File

@@ -77,6 +77,7 @@ export const CUSTOMIZED_CSS_VARS_NAME = "css";
export const MAX_AP = 57;
export const TEN_MINUTES_IN_MS = 10 * 60 * 1000;
export const HALF_HOUR_IN_MS = 30 * 60 * 1000;
export const ONE_HOUR_IN_MS = 60 * 60 * 1000;

View File

@@ -8,6 +8,7 @@ import {
FRONT_GIRL_PATH,
LOG_OUT_URL,
navIconUrl,
tournamentPage,
userPage,
} from "~/utils/urls";
import { useTranslation } from "react-i18next";
@@ -26,8 +27,17 @@ import { useRootLoaderData } from "~/hooks/useRootLoaderData";
import { useTheme } from "~/features/theme/core/provider";
import { useUser } from "~/features/auth/core/user";
import { languages } from "~/modules/i18n/config";
import type { RootLoaderData } from "~/root";
import "~/styles/front.css";
import {
HACKY_resolvePicture,
HACKY_resolveThemeColors,
} from "~/features/tournament/tournament-utils";
import { databaseTimestampToDate } from "~/utils/dates";
import { Placement } from "~/components/Placement";
import { useIsMounted } from "~/hooks/useIsMounted";
import clsx from "clsx";
export default function FrontPage() {
const data = useRootLoaderData();
@@ -49,7 +59,11 @@ export default function FrontPage() {
Log-in is temporarily disabled due to problems with the Discord API
</div>
)}
<TopBanner />
<div className="stack horizontal sm">
{data.tournaments.map((tournament) => (
<TournamentCard key={tournament.id} tournament={tournament} />
))}
</div>
<div className="front__nav-items-container">
<div className="front__nav-item round">
<LanguageChanger plain>
@@ -109,26 +123,85 @@ export default function FrontPage() {
);
}
// TODO: make into a dynamic feature
function TopBanner() {
const eventInThePast = new Date() > new Date("2023-12-03T22:00:00Z");
function TournamentCard({
tournament,
}: {
tournament: RootLoaderData["tournaments"][number];
}) {
const { t } = useTranslation(["common"]);
const isMounted = useIsMounted();
const { i18n } = useTranslation();
const theme = HACKY_resolveThemeColors(tournament);
if (eventInThePast) return null;
const happeningNow =
tournament.firstPlacers.length === 0 &&
databaseTimestampToDate(tournament.startTime) < new Date();
const rtf = new Intl.RelativeTimeFormat(i18n.language, { numeric: "auto" });
const time = () => {
if (!isMounted) return "Placeholder";
if (happeningNow) return t("common:showcase.liveNow");
const date = databaseTimestampToDate(tournament.startTime);
const dayDifference = Math.floor(
(date.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
);
if (tournament.firstPlacers.length > 0)
return rtf.format(dayDifference, "day");
return databaseTimestampToDate(tournament.startTime).toLocaleString(
i18n.language,
{
month: "numeric",
day: "numeric",
hour: "numeric",
},
);
};
return (
<div className="front__top-banner">
SendouQ Season 1 Finale tournament featuring Jackpot, FTWin, Starburst,
Alliance Rogue and more top level teams! Catch the action{" "}
<a
href="https://www.twitch.tv/iplsplatoon"
className="front__top-banner__link"
target="_blank"
rel="noopener noreferrer"
>
via IPL
</a>{" "}
on Sunday 3rd @ 10AM PST / 1PM EST / 6PM GMT
</div>
<Link
to={tournamentPage(tournament.id)}
className="front__tournament-card"
style={{
"--card-bg": theme.bg,
"--card-text": theme.text,
}}
>
<div className="stack horizontal justify-between items-center">
<Avatar url={HACKY_resolvePicture(tournament)} size="xxs" />
<div
className={clsx("front__tournament-card__time", {
invisible: !isMounted,
})}
>
{time()}
</div>
</div>
<div className="front__tournament-card__name">{tournament.name}</div>
{tournament.firstPlacers.length > 0 ? (
<>
<div />
<div className="mx-auto stack horizontal sm items-center text-xs">
<Placement placement={1} size={16} />
{tournament.firstPlacers[0].teamName}
</div>
<ul className="front__tournament-card__first-placers">
{tournament.firstPlacers.map((p) => (
<li key={p.id}>{p.discordName}</li>
))}
</ul>
</>
) : (
<div className="front__tournament-card__register">
{happeningNow
? t("common:showcase.bracket")
: t("common:showcase.register")}
</div>
)}
</Link>
);
}

View File

@@ -182,6 +182,58 @@ export async function findById(id: number) {
};
}
const NEXT_TOURNAMENTS_TO_SHOW_WITH_UPCOMING = 2;
export async function forShowcase() {
const rows = await db
.selectFrom("Tournament")
.innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId")
.innerJoin(
"CalendarEventDate",
"CalendarEvent.id",
"CalendarEventDate.eventId",
)
.select((eb) => [
"Tournament.id",
"CalendarEvent.name",
"CalendarEventDate.startTime",
jsonArrayFrom(
eb
.selectFrom("TournamentResult")
.innerJoin("User", "TournamentResult.userId", "User.id")
.innerJoin(
"TournamentTeam",
"TournamentResult.tournamentTeamId",
"TournamentTeam.id",
)
.whereRef("TournamentResult.tournamentId", "=", "Tournament.id")
.where("TournamentResult.placement", "=", 1)
.select([
"User.id",
"User.discordName",
"TournamentTeam.name as teamName",
]),
).as("firstPlacers"),
])
.orderBy("CalendarEventDate.startTime desc")
.execute();
const latestWinners = rows.find((r) => r.firstPlacers.length > 0);
const next: typeof rows = [];
const nextTournamentsCount = latestWinners
? NEXT_TOURNAMENTS_TO_SHOW_WITH_UPCOMING
: NEXT_TOURNAMENTS_TO_SHOW_WITH_UPCOMING + 1;
for (const row of rows) {
if (row.id === latestWinners?.id) break;
next.unshift(row);
if (next.length > nextTournamentsCount) next.pop();
}
return [latestWinners, ...next].filter(Boolean);
}
export async function findCastTwitchAccountsByTournamentId(
tournamentId: number,
) {

View File

@@ -79,6 +79,36 @@ export function HACKY_resolvePicture(event: { name: string }) {
return tournamentLogoUrl("default");
}
const BLACK = "#1e1e1e";
const WHITE = "#fffcfc";
export function HACKY_resolveThemeColors(event: { name: string }) {
if (HACKY_isInviteOnlyEvent(event)) {
return { bg: "#1e1e1e", text: WHITE };
}
if (event.name.includes("Paddling Pool")) {
return { bg: "#fff", text: BLACK };
}
if (event.name.includes("In The Zone")) {
return { bg: "#8b0000", text: BLACK };
}
if (event.name.includes("PICNIC")) {
return { bg: "#e3fefe", text: BLACK };
}
if (event.name.includes("Proving Grounds")) {
return { bg: "#ffe809", text: BLACK };
}
if (event.name.includes("Triton")) {
return { bg: "#aee8ff", text: BLACK };
}
return { bg: "#3430ad", text: WHITE };
}
const HACKY_isSendouQSeasonFinale = (event: { name: string }) =>
event.name.includes("Finale");

View File

@@ -24,7 +24,11 @@ import { type SendouRouteHandle } from "~/utils/remix";
import { Catcher } from "./components/Catcher";
import { ConditionalScrollRestoration } from "./components/ConditionalScrollRestoration";
import { Layout } from "./components/layout";
import { CUSTOMIZED_CSS_VARS_NAME } from "./constants";
import {
CUSTOMIZED_CSS_VARS_NAME,
ONE_HOUR_IN_MS,
TEN_MINUTES_IN_MS,
} from "./constants";
import { getUser } from "./features/auth/core/user.server";
import {
Theme,
@@ -39,6 +43,9 @@ import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
import i18next, { i18nCookie } from "./modules/i18n/i18next.server";
import { browserTimingHeader } from "./utils/newrelic.server";
import { COMMON_PREVIEW_IMAGE } from "./utils/urls";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import { cache, ttl } from "~/utils/cache.server";
import cachified from "@epic-web/cachified";
import "nprogress/nprogress.css";
import "~/styles/common.css";
@@ -80,6 +87,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
locale,
theme: themeSession.getTheme(),
patrons: await UserRepository.findAllPatrons(),
tournaments: await cachified({
key: "tournament-showcase",
cache,
ttl: ttl(TEN_MINUTES_IN_MS),
staleWhileRevalidate: ttl(ONE_HOUR_IN_MS),
async getFreshValue() {
return TournamentRepository.forShowcase();
},
}),
baseUrl: process.env["BASE_URL"]!,
skalopUrl: process.env["SKALOP_WS_URL"]!,
publisherId: process.env["PLAYWIRE_PUBLISHER_ID"],

View File

@@ -1,19 +1,3 @@
.front__top-banner {
border-radius: var(--rounded);
background: linear-gradient(to right, #4568dc, #b06ab3);
font-weight: var(--bold);
color: var(--button-text);
font-size: var(--fonts-sm);
padding-block: var(--s-1);
padding-inline: var(--s-2);
text-align: center;
}
.front__top-banner__link {
color: var(--button-text);
text-decoration: underline;
}
.front__nav-items-container {
display: flex;
margin: 0 auto;
@@ -131,3 +115,60 @@
display: none;
}
}
.front__tournament-card {
background-color: var(--card-bg);
border-radius: var(--rounded-sm);
padding: var(--s-1) var(--s-2);
flex: 1;
display: flex;
flex-direction: column;
gap: var(--s-1);
color: var(--card-text) !important;
}
.front__tournament-card__time {
font-size: var(--fonts-xxs);
font-weight: var(--bold);
opacity: 0.8;
text-transform: capitalize;
}
.front__tournament-card__first-placers {
opacity: 0.8;
font-size: var(--fonts-xxs);
list-style: none;
display: flex;
padding-left: 0;
justify-content: center;
gap: var(--s-1-5);
flex-wrap: wrap;
}
.front__tournament-card__first-placers li {
max-width: 75px;
text-overflow: ellipsis;
overflow-x: hidden;
white-space: nowrap;
}
.front__tournament-card__name {
font-size: var(--fonts-sm);
font-weight: var(--bold);
text-align: center;
margin-top: -3px;
}
.front__tournament-card__register {
font-weight: var(--semi-bold);
font-size: var(--fonts-xs);
text-align: center;
border-radius: var(--rounded-sm);
border: 1.5px solid var(--card-text);
margin-block: auto var(--s-1);
max-width: 100px;
margin-inline: auto;
padding: var(--s-0-5) var(--s-3);
background-color: var(--card-text);
color: var(--card-bg);
}

View File

@@ -215,5 +215,9 @@
"chat.systemMsg.cancelConfirmed": "{{name}} confirmed canceling the match. Match is now locked",
"chat.systemMsg.userLeft": "{{name}} left the group",
"fc.title": "Friend code"
"fc.title": "Friend code",
"showcase.register": "Register",
"showcase.bracket": "Bracket",
"showcase.liveNow": "Live now!"
}