mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-11 05:05:07 -05:00
* Read in x rank placements script
* Xsearch initial
* XSearch initial
* XSearch with select
* Add badges
* XSearch player page initial
* Consider only one build by user for popular builds Closes #1312
* Revert "Side nav labels on hover (#1290)"
This reverts commit 6e839c6c2d.
* Added and fixed DAnish translations (#1315)
* Added Danish translations
Added Danish translations for the builds.json file.
Translations for the following variables have been added
stats.count.title # used the Danish word for "average" instead of "stats", as it makes more sense in Danish.
stats.ap.title
stats.percentage.title
stats.all
linkButton.abilityStats
linkButton.popularBuilds
noPopularBuilds
* Added Danish translations
Added Danish translations in the calander.json file
tag.desc.SZ
tag.desc.TW
tag.desc.S1
tag.desc.S2
tag.desc.SR
tag.desc.CARDS
* Added Danish translations
Added Danish translations in the common.json file
The following translations have been added
pages.vods
tag.name.SZ
tag.name.TW
tag.name.S1
tag.name.S2
tag.name.SR
tag.name.CARDS
* Added Danish translations in the faq.json file
Added the following Danish translations in the faq.json file
q7
a7
* Updated the DA/game-misc.json file
Added the names for the 3.0 maps.
* Added Danish translation for team.json
Added the following Danish translations for team.json file
- roles.MIDLINE
Fixed the following translation for the team.json file
- "roles.FRONTLINE": # fixed a typo
* Created a vods.json file for the Danish trans
* Fixed typo
Fixed the following translation in the Da\contributions.json file
"yaga" # fixed a Typo of "våben"
* Add Chinese Translation (#1314)
* Update contributions.json
* Update contributions.json
* Update faq.json
* Update user.json
* Create team.json
* Update common.json
* Update analyzer.json
* Prettier
* Prettier
* Prettier
* Update builds.json
* Update calendar.json
* Update common.json
* Update faq.json
* Update team.json
* Create vods.json
* Admin link player action
* Make PlacementTable rounded if only child
* Fix arrow disappeared
* Placements on user page initial
* Remove S2 site link
* Add badge
* Add feature to README
* Fix type error
* Different badge text if XP badge
* Add badge winners script
* Better user avatar + name wrapping for mobile
* Allow script to skip users
* Fix bad align when no weapons in weapon pool
* Add aliases to player name
* Add division icon to placements table
* Link to user page
* Link to xsearch on player details page
* Top 500 icons to user build page
* Working query but slow for weapons page
* Fix lint complaints
* Add cache to builds
* Remove useless SWR value
* Group months in xsearch
* Add xsearch to nav
* Add meta
* Remove TW for now
* Add splatoon3.ink as contributor
* Remove unneeded TODO
* Fix TODOs related to fetching monthYears
* Add FAQ question
* Leaderboards to nav
* Fix sploosh build stat pages returning 404
* Add badge
* Add article
* Patch 3.1
* Fix Prettier issue
* Add badge
* Add April SR gear
* Add badges
* Rename badge
* Add badge
* Add zh badge translation (#1322)
* Splatoon Competitive Guide article (#1316)
* Create splatoon-competitive-guide.md
* Update splatoon-competitive-guide.md
* Fix ToC Issues
* Update splatoon-competitive-guide.md
* Update splatoon-competitive-guide.md
* Update splatoon-competitive-guide.md
* Corrections
* Formatting
---------
Co-authored-by: Kalle <38327916+Sendouc@users.noreply.github.com>
* Fix typos
* Rename table and adjust columns
* Make cache ttl 0 in dev
* Make placements container a bit nicer for mobile
* Rename leftover xxx
* Placements script allow passing number as arg
* Skip leaderboards for now
* Add translations
* Rename placements folder to top-search
* Add placements to seed
* Add E2E tests
* Read in x rank placements script
* Xsearch initial
* XSearch initial
* XSearch with select
* XSearch player page initial
* Admin link player action
* Make PlacementTable rounded if only child
* Fix arrow disappeared
* Placements on user page initial
* Fix bad align when no weapons in weapon pool
* Add aliases to player name
* Add division icon to placements table
* Link to user page
* Link to xsearch on player details page
* Top 500 icons to user build page
* Working query but slow for weapons page
* Fix lint complaints
* Add cache to builds
* Remove useless SWR value
* Group months in xsearch
* Add xsearch to nav
* Add meta
* Remove TW for now
* Add splatoon3.ink as contributor
* Remove unneeded TODO
* Fix TODOs related to fetching monthYears
* Add FAQ question
* Leaderboards to nav
* Fix sploosh build stat pages returning 404
* Rename table and adjust columns
* Make cache ttl 0 in dev
* Make placements container a bit nicer for mobile
* Rename leftover xxx
* Placements script allow passing number as arg
* Skip leaderboards for now
* Add translations
* Rename placements folder to top-search
* Add placements to seed
* Add E2E tests
* Rename url variable and unify
* Tweaks
---------
Co-authored-by: Frederik <112972665+FrederikFraJylland@users.noreply.github.com>
Co-authored-by: Gell <61431488+gellneko@users.noreply.github.com>
Co-authored-by: Zen <99558412+zenpk@users.noreply.github.com>
Co-authored-by: Teddi <83455454+teddinotteddy@users.noreply.github.com>
289 lines
7.6 KiB
TypeScript
289 lines
7.6 KiB
TypeScript
import { Link, useMatches } from "@remix-run/react";
|
||
import clsx from "clsx";
|
||
import * as React from "react";
|
||
import invariant from "tiny-invariant";
|
||
import { Avatar } from "~/components/Avatar";
|
||
import { Badge } from "~/components/Badge";
|
||
import { Flag } from "~/components/Flag";
|
||
import { TwitchIcon } from "~/components/icons/Twitch";
|
||
import { TwitterIcon } from "~/components/icons/Twitter";
|
||
import { YouTubeIcon } from "~/components/icons/YouTube";
|
||
import { Image, WeaponImage } from "~/components/Image";
|
||
import { useTranslation } from "~/hooks/useTranslation";
|
||
import { modesShort } from "~/modules/in-game-lists";
|
||
import { type SendouRouteHandle } from "~/utils/remix";
|
||
import { rawSensToString } from "~/utils/strings";
|
||
import type { Unpacked } from "~/utils/types";
|
||
import { assertUnreachable } from "~/utils/types";
|
||
import {
|
||
modeImageUrl,
|
||
teamPage,
|
||
userSubmittedImage,
|
||
topSearchPlayerPage,
|
||
} from "~/utils/urls";
|
||
import { badgeExplanationText } from "../badges/$id";
|
||
import type { UserPageLoaderData } from "../u.$identifier";
|
||
|
||
export const handle: SendouRouteHandle = {
|
||
i18n: "badges",
|
||
};
|
||
|
||
export default function UserInfoPage() {
|
||
const [, parentRoute] = useMatches();
|
||
invariant(parentRoute);
|
||
const data = parentRoute.data as UserPageLoaderData;
|
||
|
||
return (
|
||
<div className="u__container">
|
||
<div className="u__avatar-container">
|
||
<Avatar user={data} size="lg" className="u__avatar" />
|
||
<div>
|
||
<h2 className="u__name">
|
||
<div>{data.discordName}</div>
|
||
<div>
|
||
<span className="u__discriminator">
|
||
<wbr />#{data.discordDiscriminator}
|
||
</span>
|
||
{data.country ? <Flag countryCode={data.country} tiny /> : null}
|
||
</div>
|
||
</h2>
|
||
<TeamInfo />
|
||
</div>
|
||
<div className="u__socials">
|
||
{data.twitch ? (
|
||
<SocialLink type="twitch" identifier={data.twitch} />
|
||
) : null}
|
||
{data.twitter ? (
|
||
<SocialLink type="twitter" identifier={data.twitter} />
|
||
) : null}
|
||
{data.youtubeId ? (
|
||
<SocialLink type="youtube" identifier={data.youtubeId} />
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<ExtraInfos />
|
||
<WeaponPool />
|
||
<TopPlacements />
|
||
<BadgeContainer badges={data.badges} key={data.id} />
|
||
{data.bio && <article>{data.bio}</article>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TeamInfo() {
|
||
const [, parentRoute] = useMatches();
|
||
invariant(parentRoute);
|
||
const { team } = parentRoute.data as UserPageLoaderData;
|
||
|
||
if (!team) return null;
|
||
|
||
return (
|
||
<Link to={teamPage(team.customUrl)} className="u__team">
|
||
{team.avatarUrl ? (
|
||
<img
|
||
alt=""
|
||
src={userSubmittedImage(team.avatarUrl)}
|
||
width={24}
|
||
height={24}
|
||
className="rounded-full"
|
||
/>
|
||
) : null}
|
||
{team.name}
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
interface SocialLinkProps {
|
||
type: "youtube" | "twitter" | "twitch";
|
||
identifier: string;
|
||
}
|
||
|
||
export function SocialLink({
|
||
type,
|
||
identifier,
|
||
}: {
|
||
type: "youtube" | "twitter" | "twitch";
|
||
identifier: string;
|
||
}) {
|
||
const href = () => {
|
||
switch (type) {
|
||
case "twitch":
|
||
return `https://www.twitch.tv/${identifier}`;
|
||
case "twitter":
|
||
return `https://www.twitter.com/${identifier}`;
|
||
case "youtube":
|
||
return `https://www.youtube.com/channel/${identifier}`;
|
||
default:
|
||
assertUnreachable(type);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<a
|
||
className={clsx("u__social-link", {
|
||
youtube: type === "youtube",
|
||
twitter: type === "twitter",
|
||
twitch: type === "twitch",
|
||
})}
|
||
href={href()}
|
||
>
|
||
<SocialLinkIcon type={type} />
|
||
</a>
|
||
);
|
||
}
|
||
|
||
function SocialLinkIcon({ type }: Pick<SocialLinkProps, "type">) {
|
||
switch (type) {
|
||
case "twitch":
|
||
return <TwitchIcon />;
|
||
case "twitter":
|
||
return <TwitterIcon />;
|
||
case "youtube":
|
||
return <YouTubeIcon />;
|
||
default:
|
||
assertUnreachable(type);
|
||
}
|
||
}
|
||
|
||
function ExtraInfos() {
|
||
const { t } = useTranslation(["user"]);
|
||
const [, parentRoute] = useMatches();
|
||
invariant(parentRoute);
|
||
const data = parentRoute.data as UserPageLoaderData;
|
||
|
||
const motionSensText =
|
||
typeof data.motionSens === "number"
|
||
? ` / ${t("user:motion")} ${rawSensToString(data.motionSens)}`
|
||
: "";
|
||
|
||
if (!data.inGameName && typeof data.stickSens !== "number") {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<div className="u__extra-infos">
|
||
{data.inGameName && (
|
||
<div className="u__extra-info">
|
||
<span className="u__extra-info__heading">{t("user:ign.short")}</span>{" "}
|
||
{data.inGameName}
|
||
</div>
|
||
)}
|
||
{typeof data.stickSens === "number" && (
|
||
<div className="u__extra-info">
|
||
<span className="u__extra-info__heading">{t("user:sens")}</span>{" "}
|
||
{t("user:stick")} {rawSensToString(data.stickSens)}
|
||
{motionSensText}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function WeaponPool() {
|
||
const [, parentRoute] = useMatches();
|
||
invariant(parentRoute);
|
||
const data = parentRoute.data as UserPageLoaderData;
|
||
|
||
if (data.weapons.length === 0) return null;
|
||
|
||
return (
|
||
<div className="stack horizontal sm justify-center">
|
||
{data.weapons.map((weapon, i) => {
|
||
return (
|
||
<div key={weapon} className="u__weapon">
|
||
<WeaponImage
|
||
testId={`${weapon}-${i + 1}`}
|
||
weaponSplId={weapon}
|
||
variant="badge"
|
||
width={38}
|
||
height={38}
|
||
/>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TopPlacements() {
|
||
const [, parentRoute] = useMatches();
|
||
invariant(parentRoute);
|
||
const data = parentRoute.data as UserPageLoaderData;
|
||
|
||
if (!data.playerId) return null;
|
||
|
||
return (
|
||
<Link
|
||
to={topSearchPlayerPage(data.playerId)}
|
||
className="u__placements"
|
||
data-testid="placements-box"
|
||
>
|
||
{modesShort.map((mode) => {
|
||
const placement = data.topPlacements[mode];
|
||
|
||
if (!placement) return null;
|
||
|
||
return (
|
||
<div key={mode} className="u__placements__mode">
|
||
<Image path={modeImageUrl(mode)} alt="" width={24} height={24} />
|
||
<div>
|
||
{placement.rank} / {placement.power}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
function BadgeContainer(props: { badges: UserPageLoaderData["badges"] }) {
|
||
const { t } = useTranslation("badges");
|
||
const [badges, setBadges] = React.useState(props.badges);
|
||
|
||
const [bigBadge, ...smallBadges] = badges;
|
||
if (!bigBadge) return null;
|
||
|
||
const setBadgeFirst = (badge: Unpacked<UserPageLoaderData["badges"]>) => {
|
||
setBadges(
|
||
badges.map((b, i) => {
|
||
if (i === 0) return badge;
|
||
if (b.code === badge.code) return badges[0]!;
|
||
|
||
return b;
|
||
})
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<div
|
||
className={clsx("u__badges", {
|
||
"justify-center": smallBadges.length === 0,
|
||
})}
|
||
>
|
||
<Badge badge={bigBadge} size={125} isAnimated />
|
||
{smallBadges.length > 0 ? (
|
||
<div className="u__small-badges">
|
||
{smallBadges.map((badge) => (
|
||
<div key={badge.id} className="u__small-badge-container">
|
||
<Badge
|
||
badge={badge}
|
||
onClick={() => setBadgeFirst(badge)}
|
||
size={48}
|
||
isAnimated
|
||
/>
|
||
{badge.count > 1 ? (
|
||
<div className="u__small-badge-count">×{badge.count}</div>
|
||
) : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
<div className="u__badge-explanation">
|
||
{badgeExplanationText(t, bigBadge)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|