mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 16:20:36 -05:00
Tournament showcase on the front page
This commit is contained in:
@@ -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%)"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
18
app/root.tsx
18
app/root.tsx
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user