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 -
+ +
+ +
+ {time()} +
+
+
{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!" }