diff --git a/app/components/layout/nav-items.json b/app/components/layout/nav-items.json
index f6d2b9829..e78c0d18c 100644
--- a/app/components/layout/nav-items.json
+++ b/app/components/layout/nav-items.json
@@ -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%)"
+ ]
}
]
diff --git a/app/constants.ts b/app/constants.ts
index 2ee6a240f..22a4ecd54 100644
--- a/app/constants.ts
+++ b/app/constants.ts
@@ -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;
diff --git a/app/features/front-page/routes/index.tsx b/app/features/front-page/routes/index.tsx
index 9e517dcae..64f0b0a32 100644
--- a/app/features/front-page/routes/index.tsx
+++ b/app/features/front-page/routes/index.tsx
@@ -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
)}
-
+
+ {data.tournaments.map((tournament) => (
+
+ ))}
+
@@ -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 (
-
- SendouQ Season 1 Finale tournament featuring Jackpot, FTWin, Starburst,
- Alliance Rogue and more top level teams! Catch the action{" "}
-
- via IPL
- {" "}
- on Sunday 3rd @ 10AM PST / 1PM EST / 6PM GMT
-
+
+
+ {tournament.name}
+ {tournament.firstPlacers.length > 0 ? (
+ <>
+
+
+
+ {tournament.firstPlacers[0].teamName}
+
+
+ {tournament.firstPlacers.map((p) => (
+ {p.discordName}
+ ))}
+
+ >
+ ) : (
+
+ {happeningNow
+ ? t("common:showcase.bracket")
+ : t("common:showcase.register")}
+
+ )}
+
);
}
diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts
index b96adacf2..ae2454422 100644
--- a/app/features/tournament/TournamentRepository.server.ts
+++ b/app/features/tournament/TournamentRepository.server.ts
@@ -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,
) {
diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts
index 8fd2b468e..0f3932dd3 100644
--- a/app/features/tournament/tournament-utils.ts
+++ b/app/features/tournament/tournament-utils.ts
@@ -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");
diff --git a/app/root.tsx b/app/root.tsx
index fc69618a4..4167476f5 100644
--- a/app/root.tsx
+++ b/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"],
diff --git a/app/styles/front.css b/app/styles/front.css
index f20951a8f..a854028f3 100644
--- a/app/styles/front.css
+++ b/app/styles/front.css
@@ -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);
+}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 64fa20102..080d85ec4 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -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!"
}