diff --git a/app/components/layout/GlobalSearch.tsx b/app/components/layout/GlobalSearch.tsx index d80b9dc29..58ab61b86 100644 --- a/app/components/layout/GlobalSearch.tsx +++ b/app/components/layout/GlobalSearch.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import type { TFunction } from "i18next"; import { Search } from "lucide-react"; import * as React from "react"; import { @@ -18,6 +19,7 @@ import { Avatar } from "~/components/Avatar"; import { Image } from "~/components/Image"; import { Input } from "~/components/Input"; import type { SearchLoaderData } from "~/features/search/routes/search"; +import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { mySlugify, navIconUrl, @@ -83,6 +85,7 @@ export function GlobalSearch() { const searchParamOpen = searchParams.get("search") === "open"; const searchParamType = searchParams.get("type"); + const searchParamWeapon = searchParams.get("weapon"); const initialSearchType = searchParamType && SEARCH_TYPES.includes(searchParamType as SearchType) ? (searchParamType as SearchType) @@ -91,10 +94,10 @@ export function GlobalSearch() { const [isOpen, setIsOpen] = React.useState(searchParamOpen); React.useEffect(() => { - if (searchParamOpen && !isOpen) { + if (searchParamOpen) { setIsOpen(true); } - }, [searchParamOpen, isOpen]); + }, [searchParamOpen]); React.useEffect(() => { setIsMac(/Mac|iPhone|iPad|iPod/.test(navigator.userAgent)); @@ -115,10 +118,11 @@ export function GlobalSearch() { const handleOpenChange = (open: boolean) => { setIsOpen(open); - if (!open && (searchParamOpen || searchParamType)) { + if (!open && (searchParamOpen || searchParamType || searchParamWeapon)) { const newParams = new URLSearchParams(searchParams); newParams.delete("search"); newParams.delete("type"); + newParams.delete("weapon"); setSearchParams(newParams, { replace: true }); } }; @@ -138,8 +142,9 @@ export function GlobalSearch() { handleOpenChange(false)} + onClose={() => setIsOpen(false)} initialSearchType={initialSearchType} + initialWeaponId={searchParamWeapon} /> @@ -148,12 +153,26 @@ export function GlobalSearch() { ); } +function resolveInitialWeapon( + weaponIdStr: string | null, + t: TFunction<["common", "weapons"]>, +): SelectedWeapon | null { + if (!weaponIdStr) return null; + const id = Number(weaponIdStr) as MainWeaponId; + if (Number.isNaN(id)) return null; + const name = t(`weapons:MAIN_${id}`); + if (!name || name === `MAIN_${id}`) return null; + return { id, name, slug: mySlugify(name) }; +} + function GlobalSearchContent({ onClose, initialSearchType, + initialWeaponId, }: { onClose: () => void; initialSearchType: SearchType | null; + initialWeaponId: string | null; }) { const { t } = useTranslation(["common", "weapons"]); const navigate = useNavigate(); @@ -162,7 +181,9 @@ function GlobalSearchContent({ initialSearchType ?? getInitialSearchType(), ); const [selectedWeapon, setSelectedWeapon] = - React.useState(null); + React.useState( + resolveInitialWeapon(initialWeaponId, t), + ); const inputRef = React.useRef(null); const listBoxRef = React.useRef(null); diff --git a/app/features/front-page/loaders/index.server.ts b/app/features/front-page/loaders/index.server.ts index ccd05188e..0d3fc74c3 100644 --- a/app/features/front-page/loaders/index.server.ts +++ b/app/features/front-page/loaders/index.server.ts @@ -1,9 +1,11 @@ import cachified from "@epic-web/cachified"; import type { Tables } from "~/db/tables"; +import { getUser } from "~/features/auth/core/user.server"; import * as Changelog from "~/features/front-page/core/Changelog.server"; import { cachedFullUserLeaderboard } from "~/features/leaderboards/core/leaderboards.server"; import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; import * as Seasons from "~/features/mmr/core/Seasons"; +import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server"; import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import type { SerializeFrom } from "~/utils/remix"; @@ -13,26 +15,35 @@ import * as ShowcaseTournaments from "../core/ShowcaseTournaments.server"; export type FrontPageLoaderData = SerializeFrom; export const loader = async () => { - const [tournaments, changelog, leaderboards, rotations] = await Promise.all([ - ShowcaseTournaments.categorizedTournamentsByUserId(null), - cachified({ - key: "front-changelog", - cache, - ttl: ttl(IN_MILLISECONDS.ONE_HOUR), - staleWhileRevalidate: ttl(IN_MILLISECONDS.TWO_HOURS), - async getFreshValue() { - return Changelog.get(); - }, - }), - cachedLeaderboards(), - SplatoonRotationRepository.findAll(), - ]); + const user = getUser(); + + const [tournaments, changelog, leaderboards, rotations, weaponPool] = + await Promise.all([ + ShowcaseTournaments.categorizedTournamentsByUserId(null), + cachified({ + key: "front-changelog", + cache, + ttl: ttl(IN_MILLISECONDS.ONE_HOUR), + staleWhileRevalidate: ttl(IN_MILLISECONDS.TWO_HOURS), + async getFreshValue() { + return Changelog.get(); + }, + }), + cachedLeaderboards(), + SplatoonRotationRepository.findAll(), + user + ? QSettingsRepository.settingsByUserId(user.id).then( + (s) => s.qWeaponPool ?? null, + ) + : Promise.resolve(null), + ]); return { tournaments, changelog, leaderboards, rotations, + weaponPool, }; }; diff --git a/app/features/front-page/routes/index.tsx b/app/features/front-page/routes/index.tsx index 8da82989c..a482954c6 100644 --- a/app/features/front-page/routes/index.tsx +++ b/app/features/front-page/routes/index.tsx @@ -4,12 +4,13 @@ import { useTranslation } from "react-i18next"; import { Link, useLoaderData } from "react-router"; import { Avatar } from "~/components/Avatar"; import { Divider } from "~/components/Divider"; -import { Image } from "~/components/Image"; +import { Image, WeaponImage } from "~/components/Image"; import { ArrowRightIcon } from "~/components/icons/ArrowRight"; import { BSKYLikeIcon } from "~/components/icons/BSKYLike"; import { BSKYReplyIcon } from "~/components/icons/BSKYReply"; import { BSKYRepostIcon } from "~/components/icons/BSKYRepost"; import { ExternalIcon } from "~/components/icons/External"; +import { navItems } from "~/components/layout/nav-items"; import { Main } from "~/components/Main"; import { TournamentCard } from "~/features/calendar/components/TournamentCard"; import { SplatoonRotations } from "~/features/front-page/components/SplatoonRotations"; @@ -41,6 +42,7 @@ export default function FrontPage() { + ); @@ -227,6 +229,61 @@ function Leaderboard({ ); } +const DISCOVER_EXCLUDED_ITEMS = new Set(["settings", "luti"]); + +function DiscoverFeatures() { + const { t } = useTranslation(["front", "common"]); + const data = useLoaderData(); + + const filteredNavItems = navItems.filter( + (item) => !DISCOVER_EXCLUDED_ITEMS.has(item.name), + ); + + return ( +
+ + {t("front:discover.header")} + + {data.weaponPool && data.weaponPool.length > 0 ? ( +
+ {data.weaponPool.map((weapon) => ( + + + + ))} +
+ ) : null} + +
+ ); +} + function ChangelogList() { const { t } = useTranslation(["front"]); const data = useLoaderData(); diff --git a/app/styles/front.module.css b/app/styles/front.module.css index 5b52184f2..8fb6024b6 100644 --- a/app/styles/front.module.css +++ b/app/styles/front.module.css @@ -165,3 +165,62 @@ gap: var(--s-2); flex-wrap: wrap; } + +.weaponPills { + display: flex; + gap: var(--s-2); + justify-content: center; + flex-wrap: wrap; +} + +.weaponPill { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-bg-higher); + border-radius: var(--radius-full); + padding: var(--s-1); + transition: background-color 0.15s ease-out; +} + +.weaponPill:hover { + background-color: var(--color-bg-high); +} + +.discoverGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--s-3); + padding: var(--s-2); +} + +@media (min-width: 600px) { + .discoverGrid { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + } +} + +.discoverGridItem { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--s-1); + text-decoration: none; + color: var(--color-text); + font-size: var(--font-xs); + font-weight: var(--weight-semi); + text-align: center; +} + +.discoverGridItem:hover .discoverGridItemImage { + background-color: var(--color-bg-high); +} + +.discoverGridItemImage { + width: var(--field-size-lg); + aspect-ratio: 1 / 1; + border-radius: var(--radius-field); + background-color: var(--color-bg-higher); + display: grid; + place-items: center; +} diff --git a/locales/da/front.json b/locales/da/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/da/front.json +++ b/locales/da/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/de/front.json b/locales/de/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/de/front.json +++ b/locales/de/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/en/front.json b/locales/en/front.json index a95e2f22a..70caa4922 100644 --- a/locales/en/front.json +++ b/locales/en/front.json @@ -38,5 +38,6 @@ "rotations.current": "Now", "rotations.nextLabel": "Next", "rotations.credit": "Data from splatoon3.ink", - "rotations.filter.all": "All" + "rotations.filter.all": "All", + "discover.header": "Discover all the features" } diff --git a/locales/es-ES/front.json b/locales/es-ES/front.json index 84e6b1c4c..f61b09feb 100644 --- a/locales/es-ES/front.json +++ b/locales/es-ES/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/es-US/front.json b/locales/es-US/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/es-US/front.json +++ b/locales/es-US/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/fr-CA/front.json b/locales/fr-CA/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/fr-CA/front.json +++ b/locales/fr-CA/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/fr-EU/front.json b/locales/fr-EU/front.json index 2701d3bc7..0bf753112 100644 --- a/locales/fr-EU/front.json +++ b/locales/fr-EU/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/he/front.json b/locales/he/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/he/front.json +++ b/locales/he/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/it/front.json b/locales/it/front.json index 3c7db623b..ca25765ba 100644 --- a/locales/it/front.json +++ b/locales/it/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ja/front.json b/locales/ja/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/ja/front.json +++ b/locales/ja/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ko/front.json b/locales/ko/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/ko/front.json +++ b/locales/ko/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/nl/front.json b/locales/nl/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/nl/front.json +++ b/locales/nl/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/pl/front.json b/locales/pl/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/pl/front.json +++ b/locales/pl/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/pt-BR/front.json b/locales/pt-BR/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/pt-BR/front.json +++ b/locales/pt-BR/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/ru/front.json b/locales/ru/front.json index 6dad8befb..4e26fabd4 100644 --- a/locales/ru/front.json +++ b/locales/ru/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" } diff --git a/locales/zh/front.json b/locales/zh/front.json index 23d7f5df5..3b3b5465f 100644 --- a/locales/zh/front.json +++ b/locales/zh/front.json @@ -38,5 +38,6 @@ "rotations.current": "", "rotations.nextLabel": "", "rotations.credit": "", - "rotations.filter.all": "" + "rotations.filter.all": "", + "discover.header": "" }