mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Add "Discover all the features" to the front page
This commit is contained in:
parent
570da9541b
commit
a1d2784a9e
|
|
@ -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() {
|
|||
<Modal className={styles.modal}>
|
||||
<Dialog className={styles.dialog} aria-label={t("common:search")}>
|
||||
<GlobalSearchContent
|
||||
onClose={() => handleOpenChange(false)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
initialSearchType={initialSearchType}
|
||||
initialWeaponId={searchParamWeapon}
|
||||
/>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
|
|
@ -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<SelectedWeapon | null>(null);
|
||||
React.useState<SelectedWeapon | null>(
|
||||
resolveInitialWeapon(initialWeaponId, t),
|
||||
);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const listBoxRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof loader>;
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<SeasonBanner />
|
||||
<SplatoonRotations />
|
||||
<ResultHighlights />
|
||||
<DiscoverFeatures />
|
||||
<ChangelogList />
|
||||
</Main>
|
||||
);
|
||||
|
|
@ -227,6 +229,61 @@ function Leaderboard({
|
|||
);
|
||||
}
|
||||
|
||||
const DISCOVER_EXCLUDED_ITEMS = new Set(["settings", "luti"]);
|
||||
|
||||
function DiscoverFeatures() {
|
||||
const { t } = useTranslation(["front", "common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const filteredNavItems = navItems.filter(
|
||||
(item) => !DISCOVER_EXCLUDED_ITEMS.has(item.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
<Divider smallText className="text-uppercase text-xs font-bold">
|
||||
{t("front:discover.header")}
|
||||
</Divider>
|
||||
{data.weaponPool && data.weaponPool.length > 0 ? (
|
||||
<div className={styles.weaponPills}>
|
||||
{data.weaponPool.map((weapon) => (
|
||||
<Link
|
||||
key={weapon.weaponSplId}
|
||||
to={`?search=open&type=weapons&weapon=${weapon.weaponSplId}`}
|
||||
className={styles.weaponPill}
|
||||
>
|
||||
<WeaponImage
|
||||
weaponSplId={weapon.weaponSplId}
|
||||
variant="badge"
|
||||
size={32}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<nav className={styles.discoverGrid}>
|
||||
{filteredNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={`/${item.url}`}
|
||||
className={styles.discoverGridItem}
|
||||
>
|
||||
<div className={styles.discoverGridItemImage}>
|
||||
<Image
|
||||
path={navIconUrl(item.name)}
|
||||
height={32}
|
||||
width={32}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<span>{t(`common:pages.${item.name}` as any)}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangelogList() {
|
||||
const { t } = useTranslation(["front"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
"rotations.current": "",
|
||||
"rotations.nextLabel": "",
|
||||
"rotations.credit": "",
|
||||
"rotations.filter.all": ""
|
||||
"rotations.filter.all": "",
|
||||
"discover.header": ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user