diff --git a/app/components/GearSelect.tsx b/app/components/GearSelect.tsx index 6c88544a4..476e4219e 100644 --- a/app/components/GearSelect.tsx +++ b/app/components/GearSelect.tsx @@ -1,4 +1,3 @@ -import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { SendouSelect, @@ -13,7 +12,6 @@ import { headGearBrandGrouped, shoesGearBrandGrouped, } from "~/modules/in-game-lists/gear-ids"; -import type { BrandId } from "~/modules/in-game-lists/types"; import { brandImageUrl, gearImageUrl } from "~/utils/urls"; import styles from "./WeaponSelect.module.css"; @@ -59,12 +57,9 @@ export function GearSelect({ > {({ key, items: gear, brandId, idx }) => ( - } + className={idx === 0 ? "pt-0-5-forced" : undefined} + heading={t(`game-misc:BRAND_${brandId}` as any)} + headingImgPath={brandImageUrl(brandId)} key={key} > {gear.map(({ id, name }) => ( @@ -91,24 +86,6 @@ export function GearSelect({ ); } -function CategoryHeading({ - className, - brandId, -}: { - className?: string; - brandId: BrandId; -}) { - const { t } = useTranslation(["game-misc"]); - - return ( -
- - {t(`game-misc:BRAND_${brandId}` as any)} -
-
- ); -} - function useGearItems(type: GearType) { const { t } = useTranslation(["gear", "game-misc"]); diff --git a/app/components/WeaponSelect.module.css b/app/components/WeaponSelect.module.css index 8c296c654..bdb895e8d 100644 --- a/app/components/WeaponSelect.module.css +++ b/app/components/WeaponSelect.module.css @@ -18,31 +18,3 @@ text-overflow: ellipsis; min-width: 0; } - -.categoryHeading { - display: flex; - align-items: center; - gap: var(--s-2); - font-weight: bold; - color: var(--text-lighter); - text-transform: uppercase; - font-size: var(--fonts-xxs); - padding-block-start: var(--s-2-5); - padding-block-end: var(--s-1); - padding-inline: var(--s-1-5); - white-space: nowrap; -} - -.categoryDivider { - background-color: var(--border); - width: 100%; - height: 2px; - margin-block: var(--s-2); -} - -.categoryHeading img { - border-radius: 100%; - background-color: var(--bg-lightest); - padding: var(--s-1); - min-width: 28px; -} diff --git a/app/components/WeaponSelect.tsx b/app/components/WeaponSelect.tsx index aaf46373b..b159dd9ba 100644 --- a/app/components/WeaponSelect.tsx +++ b/app/components/WeaponSelect.tsx @@ -1,4 +1,3 @@ -import clsx from "clsx"; import * as React from "react"; import type { Key } from "react-aria-components"; import { useTranslation } from "react-i18next"; @@ -110,12 +109,15 @@ export function WeaponSelect< > {({ key, items: weapons, name, idx }) => ( + heading={name} + headingImgPath={ + name === "subs" + ? subWeaponImageUrl(SPLAT_BOMB_ID) + : name === "specials" + ? specialWeaponImageUrl(TRIZOOKA_ID) + : weaponCategoryUrl(name) } + className={idx === 0 ? "pt-0-5-forced" : undefined} key={key} > {weapons.map(({ weapon, name }) => ( @@ -167,31 +169,6 @@ export function WeaponSelect< ); } -function CategoryHeading({ - name, - className, -}: { - name: (typeof weaponCategories)[number]["name"] | "subs" | "specials"; - className?: string; -}) { - const { t } = useTranslation(["common"]); - - const path = () => { - if (name === "subs") return subWeaponImageUrl(SPLAT_BOMB_ID); - if (name === "specials") return specialWeaponImageUrl(TRIZOOKA_ID); - - return weaponCategoryUrl(name); - }; - - return ( -
- - {t(`common:weapon.category.${name}`)} -
-
- ); -} - function useFilteredWeaponItems(includeSubSpecial: boolean | undefined) { const items = useAllWeaponCategories(includeSubSpecial); const [filterValue, setFilterValue] = React.useState(""); diff --git a/app/components/elements/Select.module.css b/app/components/elements/Select.module.css index 80398baaa..b4c7e42f2 100644 --- a/app/components/elements/Select.module.css +++ b/app/components/elements/Select.module.css @@ -151,3 +151,31 @@ bottom: -17px; right: 9px; } + +.categoryHeading { + display: flex; + align-items: center; + gap: var(--s-2); + font-weight: bold; + color: var(--text-lighter); + text-transform: uppercase; + font-size: var(--fonts-xxs); + padding-block-start: var(--s-2-5); + padding-block-end: var(--s-1); + padding-inline: var(--s-1-5); + white-space: nowrap; +} + +.categoryDivider { + background-color: var(--border); + width: 100%; + height: 2px; + margin-block: var(--s-2); +} + +.categoryHeading img { + border-radius: 100%; + background-color: var(--bg-lightest); + padding: var(--s-1); + min-width: 28px; +} diff --git a/app/components/elements/Select.tsx b/app/components/elements/Select.tsx index b508fd522..2b8f5b521 100644 --- a/app/components/elements/Select.tsx +++ b/app/components/elements/Select.tsx @@ -23,6 +23,7 @@ import { useTranslation } from "react-i18next"; import { SendouBottomTexts } from "~/components/elements/BottomTexts"; import { SendouButton } from "~/components/elements/Button"; import { ChevronUpDownIcon } from "~/components/icons/ChevronUpDown"; +import { Image } from "../Image"; import { CrossIcon } from "../icons/Cross"; import { SearchIcon } from "../icons/Search"; import styles from "./Select.module.css"; @@ -159,17 +160,27 @@ export function SendouSelectItem(props: SendouSelectItemProps) { } interface SendouSelectItemSectionProps { - heading: React.ReactNode; + heading: string; + headingImgPath?: string; children: React.ReactNode; + className?: string; } export function SendouSelectItemSection({ heading, + headingImgPath, children, + className, }: SendouSelectItemSectionProps) { return ( -
{heading}
+
+ {headingImgPath ? ( + + ) : null} + {heading} +
+
{children}
); diff --git a/app/features/leaderboards/LeaderboardRepository.server.ts b/app/features/leaderboards/LeaderboardRepository.server.ts index 77af4a2df..554c96424 100644 --- a/app/features/leaderboards/LeaderboardRepository.server.ts +++ b/app/features/leaderboards/LeaderboardRepository.server.ts @@ -231,3 +231,15 @@ function ignoreTeams({ return true; }); } + +export async function seasonsParticipatedInByUserId(userId: number) { + const rows = await db + .selectFrom("Skill") + .select("season") + .where("userId", "=", userId) + .groupBy("season") + .orderBy("season", "desc") + .execute(); + + return rows.map((row) => row.season); +} diff --git a/app/features/user-page/loaders/u.$identifier.seasons.server.ts b/app/features/user-page/loaders/u.$identifier.seasons.server.ts index 427f8459a..ee5e54723 100644 --- a/app/features/user-page/loaders/u.$identifier.seasons.server.ts +++ b/app/features/user-page/loaders/u.$identifier.seasons.server.ts @@ -1,5 +1,5 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; -import * as Seasons from "~/features/mmr/core/Seasons"; +import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; import { seasonAllMMRByUserId } from "~/features/mmr/queries/seasonAllMMRByUserId.server"; import { userSkills as _userSkills } from "~/features/mmr/tiered.server"; import { seasonMapWinrateByUserId } from "~/features/sendouq/queries/seasonMapWinrateByUserId.server"; @@ -12,26 +12,38 @@ import { seasonSetWinrateByUserId } from "~/features/sendouq/queries/seasonSetWi import { seasonStagesByUserId } from "~/features/sendouq/queries/seasonStagesByUserId.server"; import { seasonsMatesEnemiesByUserId } from "~/features/sendouq/queries/seasonsMatesEnemiesByUserId.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; +import type { SerializeFrom } from "~/utils/remix"; import { notFoundIfFalsy } from "~/utils/remix.server"; import { seasonsSearchParamsSchema, userParamsSchema, } from "../user-page-schemas"; +export type UserSeasonsPageLoaderData = NonNullable< + SerializeFrom +>; + export const loader = async ({ params, request }: LoaderFunctionArgs) => { const { identifier } = userParamsSchema.parse(params); const parsedSearchParams = seasonsSearchParamsSchema.safeParse( Object.fromEntries(new URL(request.url).searchParams), ); - const { - info = "weapons", - page = 1, - season = Seasons.currentOrPrevious()!.nth, - } = parsedSearchParams.success ? parsedSearchParams.data : {}; const user = notFoundIfFalsy( await UserRepository.identifierToUserId(identifier), ); + const seasonsParticipatedIn = + await LeaderboardRepository.seasonsParticipatedInByUserId(user.id); + + if (seasonsParticipatedIn.length === 0) { + return null; + } + + const { + info = "weapons", + page = 1, + season = seasonsParticipatedIn[0], + } = parsedSearchParams.success ? parsedSearchParams.data : {}; const { isAccurateTiers, userSkills } = _userSkills(season); const { tier, ordinal, approximate } = userSkills[user.id] ?? { @@ -41,6 +53,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { }; return { + seasonsParticipatedIn, currentOrdinal: !approximate ? ordinal : undefined, winrates: { maps: seasonMapWinrateByUserId({ season, userId: user.id }), diff --git a/app/features/user-page/routes/u.$identifier.seasons.tsx b/app/features/user-page/routes/u.$identifier.seasons.tsx index cb3a1ee7e..12af048e9 100644 --- a/app/features/user-page/routes/u.$identifier.seasons.tsx +++ b/app/features/user-page/routes/u.$identifier.seasons.tsx @@ -1,8 +1,8 @@ -import type { SerializeFrom } from "@remix-run/node"; import { Link, useLoaderData, useMatches, + useNavigate, useSearchParams, } from "@remix-run/react"; import clsx from "clsx"; @@ -12,6 +12,11 @@ import { Avatar } from "~/components/Avatar"; import Chart from "~/components/Chart"; import { SendouButton } from "~/components/elements/Button"; import { SendouPopover } from "~/components/elements/Popover"; +import { + SendouSelect, + SendouSelectItem, + SendouSelectItemSection, +} from "~/components/elements/Select"; import { SendouTab, SendouTabList, @@ -43,7 +48,10 @@ import { cutToNDecimalPlaces, roundToNDecimalPlaces } from "~/utils/number"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { sendouQMatchPage, TIERS_PAGE, userSeasonsPage } from "~/utils/urls"; -import { loader } from "../loaders/u.$identifier.seasons.server"; +import { + loader, + type UserSeasonsPageLoaderData, +} from "../loaders/u.$identifier.seasons.server"; import type { UserPageLoaderData } from "../loaders/u.$identifier.server"; export { loader }; @@ -56,13 +64,21 @@ export default function UserSeasonsPage() { const { t } = useTranslation(["user"]); const data = useLoaderData(); - const tabLink = (tab: string) => - `?info=${tab}&page=${data.matches.currentPage}&season=${data.season}`; + if (!data) { + return ( +
+ {t("user:seasons.noSeasons")} +
+ ); + } if (data.matches.value.length === 0) { return (
- +
{t("user:seasons.noQ")}
@@ -70,17 +86,29 @@ export default function UserSeasonsPage() { ); } + const tabLink = (tab: string) => + `?info=${tab}&page=${data.matches.currentPage}&season=${data.season}`; + return (
- + {data.currentOrdinal ? (
- + {data.winrates.maps.wins + data.winrates.maps.losses > 0 ? ( - + ) : null} {data.skills.length >= DAYS_WITH_SKILL_NEEDED_TO_SHOW_POWER_CHART ? ( - + ) : null}
) : null} @@ -121,47 +149,58 @@ export default function UserSeasonsPage() {
{data.info.weapons ? : null} - {data.info.stages ? : null} - {data.info.players ? : null} + {data.info.stages ? ( + + ) : null} + {data.info.players ? ( + + ) : null}
- +
); } -function SeasonHeader() { - const { t, i18n } = useTranslation(["user"]); - const data = useLoaderData(); +function SeasonHeader({ + seasonViewed, + seasonsParticipatedIn, +}: { + seasonViewed: number; + seasonsParticipatedIn: number[]; +}) { + const { i18n } = useTranslation(["user"]); const isMounted = useIsMounted(); - const { starts, ends } = Seasons.nthToDateRange(data.season); + const { starts, ends } = Seasons.nthToDateRange(seasonViewed); + const navigate = useNavigate(); + const options = useSeasonSelectOptions(); const isDifferentYears = new Date(starts).getFullYear() !== new Date(ends).getFullYear(); return (
-
- {Seasons.allStarted().map((s) => { - const isActive = s === data.season; - - return ( - - {isActive - ? `${t("user:seasons.season")} ` - : t("user:seasons.season.short")} - {s} - - ); - })} -
+ navigate(`?season=${seasonNth}`)} + items={options} + className="u__season__select" + popoverClassName="u__season__select" + > + {({ year, items, key }) => ( + + {items.map((item) => ( + + {item.name} + + ))} + + )} +
{isMounted ? ( <> @@ -185,9 +224,42 @@ function SeasonHeader() { ); } -function Winrates() { +function useSeasonSelectOptions() { + const { t } = useTranslation(["user"]); + + const seasonSelectItems = Seasons.allStarted().map((seasonNth) => ({ + seasonNth, + key: seasonNth, + name: `${t("user:seasons.season")} ${seasonNth}`, + })); + + const groupedSeasonItems = seasonSelectItems.reduce( + (acc, item) => { + const year = Seasons.nthToDateRange(item.seasonNth).starts.getFullYear(); + if (!acc[year]) { + acc[year] = []; + } + acc[year].push(item); + return acc; + }, + {} as Record, + ); + + return Object.entries(groupedSeasonItems) + .sort(([yearA], [yearB]) => Number(yearB) - Number(yearA)) + .map(([year, items]) => ({ + year, + items: items.sort((a, b) => b.seasonNth - a.seasonNth), + key: year, + })); +} + +function Winrates({ + winrates, +}: { + winrates: UserSeasonsPageLoaderData["winrates"]; +}) { const { t } = useTranslation(["user"]); - const data = useLoaderData(); const winrate = (wins: number, losses: number) => Math.round((wins / (wins + losses)) * 100); @@ -195,48 +267,57 @@ function Winrates() { return (
- Sets{" "} - {data.winrates.sets.wins} - {t("user:seasons.win.short")} {data.winrates.sets.losses} + Sets {winrates.sets.wins} + {t("user:seasons.win.short")} {winrates.sets.losses} {t("user:seasons.loss.short")} ( - {winrate(data.winrates.sets.wins, data.winrates.sets.losses)}%) + {winrate(winrates.sets.wins, winrates.sets.losses)}%)
- Maps{" "} - {data.winrates.maps.wins} - {t("user:seasons.win.short")} {data.winrates.maps.losses} + Maps {winrates.maps.wins} + {t("user:seasons.win.short")} {winrates.maps.losses} {t("user:seasons.loss.short")} ( - {winrate(data.winrates.maps.wins, data.winrates.maps.losses)}%) + {winrate(winrates.maps.wins, winrates.maps.losses)}%)
); } -function Rank({ currentOrdinal }: { currentOrdinal: number }) { +function Rank({ + currentOrdinal, + seasonViewed, + tier, + isAccurateTiers, + skills, +}: { + currentOrdinal: number; + seasonViewed: number; + tier: UserSeasonsPageLoaderData["tier"]; + isAccurateTiers: UserSeasonsPageLoaderData["isAccurateTiers"]; + skills: UserSeasonsPageLoaderData["skills"]; +}) { const { t } = useTranslation(["user"]); - const data = useLoaderData(); const [, parentRoute] = useMatches(); invariant(parentRoute); const layoutData = parentRoute.data as UserPageLoaderData; - const maxOrdinal = Math.max(...data.skills.map((s) => s.ordinal)); + const maxOrdinal = Math.max(...skills.map((s) => s.ordinal)); const peakAndCurrentSame = currentOrdinal === maxOrdinal; const topTenPlacement = playerTopTenPlacement({ - season: data.season, + season: seasonViewed, userId: layoutData.user.id, }); return (
- +
- {data.tier.name} - {data.tier.isPlus ? "+" : ""} + {tier.name} + {tier.isPlus ? "+" : ""} - {!data.isAccurateTiers ? ( + {!isAccurateTiers ? (
{t("user:seasons.tentative")}{" "} ) : null}
@@ -271,14 +352,16 @@ function Rank({ currentOrdinal }: { currentOrdinal: number }) { ); } -function PowerChart() { - const data = useLoaderData(); - +function PowerChart({ + skills, +}: { + skills: UserSeasonsPageLoaderData["skills"]; +}) { const chartOptions = React.useMemo(() => { return [ { label: "SP", - data: data.skills.map((s) => { + data: skills.map((s) => { return { primary: new Date(s.date), secondary: ordinalToSp(s.ordinal), @@ -286,7 +369,7 @@ function PowerChart() { }), }, ]; - }, [data]); + }, [skills]); return ; } @@ -296,7 +379,7 @@ const WEAPONS_TO_SHOW = 9; function Weapons({ weapons, }: { - weapons: NonNullable["info"]["weapons"]>; + weapons: NonNullable; }) { const { t } = useTranslation(["user", "weapons"]); @@ -345,11 +428,12 @@ function Weapons({ } function Stages({ + seasonViewed, stages, }: { - stages: NonNullable["info"]["stages"]>; + seasonViewed: number; + stages: NonNullable; }) { - const data = useLoaderData(); const { t } = useTranslation(["user", "game-misc"]); const layoutData = atOrError(useMatches(), -2).data as UserPageLoaderData; @@ -390,7 +474,7 @@ function Stages({ > @@ -493,11 +577,12 @@ function StageWeaponUsageStats(props: { function Players({ players, + seasonViewed, }: { - players: NonNullable["info"]["players"]>; + players: NonNullable; + seasonViewed: number; }) { const { t } = useTranslation(["user"]); - const data = useLoaderData(); return (
@@ -511,7 +596,7 @@ function Players({ return (
@@ -562,22 +647,27 @@ function WeaponCircle({ ); } -function Matches() { +function Matches({ + seasonViewed, + matches, +}: { + seasonViewed: number; + matches: UserSeasonsPageLoaderData["matches"]; +}) { const isMounted = useIsMounted(); - const data = useLoaderData(); const [, setSearchParams] = useSearchParams(); const ref = React.useRef(null); const setPage = (page: number) => { - setSearchParams({ page: String(page), season: String(data.season) }); + setSearchParams({ page: String(page), season: String(seasonViewed) }); }; React.useEffect(() => { - if (data.matches.currentPage === 1) return; + if (matches.currentPage === 1) return; ref.current?.scrollIntoView({ block: "center", }); - }, [data.matches.currentPage]); + }, [matches.currentPage]); let lastDayRendered: number | null = null; return ( @@ -585,7 +675,7 @@ function Matches() {
- {data.matches.value.map((match) => { + {matches.value.map((match) => { const day = databaseTimestampToDate(match.createdAt).getDate(); const shouldRenderDateHeader = day !== lastDayRendered; lastDayRendered = day; @@ -616,12 +706,12 @@ function Matches() { ); })}
- {data.matches.pages > 1 ? ( + {matches.pages > 1 ? ( setPage(data.matches.currentPage + 1)} - previousPage={() => setPage(data.matches.currentPage - 1)} + currentPage={matches.currentPage} + pagesCount={matches.pages} + nextPage={() => setPage(matches.currentPage + 1)} + previousPage={() => setPage(matches.currentPage - 1)} setPage={(page) => setPage(page)} /> ) : null} @@ -633,7 +723,7 @@ function Matches() { function Match({ match, }: { - match: SerializeFrom["matches"]["value"][0]; + match: UserSeasonsPageLoaderData["matches"]["value"][0]; }) { const { t } = useTranslation(["user"]); const [, parentRoute] = useMatches(); @@ -728,9 +818,7 @@ function MatchMembersRow({ reserveWeaponSpace, }: { score: React.ReactNode; - members: SerializeFrom< - typeof loader - >["matches"]["value"][0]["groupAlphaMembers"]; + members: UserSeasonsPageLoaderData["matches"]["value"][0]["groupAlphaMembers"]; reserveWeaponSpace: boolean; }) { return ( diff --git a/app/styles/u.css b/app/styles/u.css index 4aa75a150..345c26353 100644 --- a/app/styles/u.css +++ b/app/styles/u.css @@ -252,6 +252,10 @@ } } +.u__season__select { + --select-width: 125px; +} + .u__season__weapon-container { display: flex; align-items: center; diff --git a/locales/da/user.json b/locales/da/user.json index d1831bfe8..b55092bd6 100644 --- a/locales/da/user.json +++ b/locales/da/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "Sæsoner", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Våben", "seasons.tabs.stages": "Baner", "seasons.tabs.self": "Self", diff --git a/locales/de/user.json b/locales/de/user.json index c3ffcedd9..948231869 100644 --- a/locales/de/user.json +++ b/locales/de/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/en/user.json b/locales/en/user.json index 5ed7c13fd..dc9ccc7b1 100644 --- a/locales/en/user.json +++ b/locales/en/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "Log in via Discord", "seasons": "Seasons", "seasons.noQ": "This user has not played SendouQ this season", + "seasons.noSeasons": "This user has not participated in any seasons", "seasons.tabs.weapons": "Weapons", "seasons.tabs.stages": "Stages", "seasons.tabs.self": "Self", diff --git a/locales/es-ES/user.json b/locales/es-ES/user.json index 185265061..81ef8d89b 100644 --- a/locales/es-ES/user.json +++ b/locales/es-ES/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "Temporadas", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Armas", "seasons.tabs.stages": "Mapas", "seasons.tabs.self": "Personal", diff --git a/locales/es-US/user.json b/locales/es-US/user.json index f507ce7ea..db63733ff 100644 --- a/locales/es-US/user.json +++ b/locales/es-US/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "Temporadas", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Armas", "seasons.tabs.stages": "Escenarios", "seasons.tabs.self": "Personal", diff --git a/locales/fr-CA/user.json b/locales/fr-CA/user.json index f0e3952cb..e8138f1b5 100644 --- a/locales/fr-CA/user.json +++ b/locales/fr-CA/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/fr-EU/user.json b/locales/fr-EU/user.json index f92d68e29..43e2d0aa8 100644 --- a/locales/fr-EU/user.json +++ b/locales/fr-EU/user.json @@ -14,7 +14,6 @@ "discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Bluesky et Twitch viennent de votre compte Discord. Voir la <1>FAQ pour plus d'informations.", "favoriteBadges": "Badge favori", "battlefy": "Nom du compte Battlefy", - "forms.showDiscordUniqueName": "Montrer le pseudo Discord", "forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?", "forms.commissionsOpen": "Commissions acceptées", @@ -23,7 +22,6 @@ "forms.customName.info": "Si il n'est pas présent, votre pseudo discord est utilisé: \"{{discordName}}\"", "forms.country.search.placeholder": "Rechercher de pays", "forms.favoriteBadges.nonSupporter": "Devenez supporter pour définir l'ordre des badges qui apparaissent sur la première page", - "results.title": "Tout les résultats", "results.placing": "Placement", "results.team": "Équipe", @@ -35,7 +33,6 @@ "results.highlights.explanation": "Sélectionnez les résultats que vous voulez mettre en avant", "results.button.showHighlights": "Montrer les highlights", "results.button.showAll": "Tout montrer", - "forms.errors.maxWeapons": "Nombre d'armes maximum atteint", "forms.errors.invalidCustomUrl.numbers": "Votre URL personnalisée ne peut pas contenir que des nombres", "forms.errors.invalidCustomUrl.strangeCharacter": "Votre URL personnalisée ne peut pas contenir de caractères spéciaux", @@ -43,14 +40,13 @@ "forms.errors.invalidSens": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas", "forms.info.customUrl": "Pour les Supporter patrons (& plus), les liens courts sont disponibles. Exemple: Au mieux de sendou.ink/u/sendou, snd.ink/sendou peut être utilisé.", "forms.info.battlefy": "Votre nom Battlefy est utiliser pour le seeding et la verification de certains tournois", - "search.info": "Recherchez avec le pseudo Discord ou Splatoon 3 du compte", "search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé", "search.pleaseLogIn.header": "Veuillez vous connecter pour rechercher des utilisateurs", "search.pleaseLogIn.button": "Connectez-vous via Discord", - "seasons": "Saison", "seasons.noQ": "Ce joueur ne joue pas en SendouQ cette saison", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Armes", "seasons.tabs.stages": "Stages", "seasons.tabs.self": "Soi", @@ -67,7 +63,6 @@ "seasons.clickARow": "Cliquez sur une ligne pour voir les statistiques d'utilisation des armes", "seasons.loading": "Chargement...", "seasons.matchBeingProcessed": "Le match n'a pas encore été terminé", - "builds.sorting.changeButton": "Changer le tri", "builds.sorting.header": "Modifier le tri des builds", "builds.sorting.backToDefaults": "Revenir par défaut", diff --git a/locales/he/user.json b/locales/he/user.json index 73a7d3124..744c46e39 100644 --- a/locales/he/user.json +++ b/locales/he/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/it/user.json b/locales/it/user.json index f7259b3b3..0910aa351 100644 --- a/locales/it/user.json +++ b/locales/it/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "Stagioni", "seasons.noQ": "Questo utente non ha giocato SendouQ in questa stagione", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Armi", "seasons.tabs.stages": "Mappe", "seasons.tabs.self": "Sé", diff --git a/locales/ja/user.json b/locales/ja/user.json index 9c6882ac4..70719032f 100644 --- a/locales/ja/user.json +++ b/locales/ja/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "シーズン", "seasons.noQ": "このプレイヤーは今シーズンSendouQをプレイしていません", + "seasons.noSeasons": "", "seasons.tabs.weapons": "武器", "seasons.tabs.stages": "ステージ", "seasons.tabs.self": "自分", diff --git a/locales/ko/user.json b/locales/ko/user.json index 28f17f6d9..eb8f39375 100644 --- a/locales/ko/user.json +++ b/locales/ko/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/nl/user.json b/locales/nl/user.json index e8f3f2578..b5b963b52 100644 --- a/locales/nl/user.json +++ b/locales/nl/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/pl/user.json b/locales/pl/user.json index 23a784348..ef7d2d52e 100644 --- a/locales/pl/user.json +++ b/locales/pl/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "", "seasons.tabs.stages": "", "seasons.tabs.self": "", diff --git a/locales/pt-BR/user.json b/locales/pt-BR/user.json index 72d850164..bad496819 100644 --- a/locales/pt-BR/user.json +++ b/locales/pt-BR/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "Temporadas", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Armas", "seasons.tabs.stages": "Mapas", "seasons.tabs.self": "Si mesmo(a)", diff --git a/locales/ru/user.json b/locales/ru/user.json index bdfd015d2..718679746 100644 --- a/locales/ru/user.json +++ b/locales/ru/user.json @@ -14,7 +14,6 @@ "discordExplanation": "Имя пользователя, аватар, ссылка на аккаунты YouTube, Bluesky и Twitch берутся из вашего аккаунта в Discord. Посмотрите <1>FAQ для дополнительной информации.", "favoriteBadges": "Любимые награды", "battlefy": "Аккаунт Battlefy", - "forms.showDiscordUniqueName": "Показать пользовательское имя Discord", "forms.showDiscordUniqueName.info": "Показывать ваше уникальное Discord имя ({{discordUniqueName}})?", "forms.commissionsOpen": "Коммишены открыты", @@ -23,7 +22,6 @@ "forms.customName.info": "Если пользовательское имя отсутствует, то будет использовано ваше имя в Discord: \"{{discordName}}\"", "forms.country.search.placeholder": "Искать страны", "forms.favoriteBadges.nonSupporter": "Станьте суппортером sendou.ink, чтобы выбрать какие награды и в каком порядке отображаются на первой странице", - "results.title": "Все результаты", "results.placing": "Место", "results.team": "Команда", @@ -35,7 +33,6 @@ "results.highlights.explanation": "Выберите ваш избранный результат", "results.button.showHighlights": "Показать избранные", "results.button.showAll": "Показать все", - "forms.errors.maxWeapons": "Достигнут максимум", "forms.errors.invalidCustomUrl.numbers": "Пользовательский URL не может содержать только цифры", "forms.errors.invalidCustomUrl.strangeCharacter": "Пользовательский URL не может содержать особые символы", @@ -43,14 +40,13 @@ "forms.errors.invalidSens": "Чувствительность наклона не может быть указана, если не указана чувствительность стика", "forms.info.customUrl": "Для меценатов (Supporter и выше) доступна короткая ссылка. Например, вместо sendou.ink/u/sendou может быть использована сссылка snd.ink/sendou.", "forms.info.battlefy": "Имя на Battlefy может быть использовано для посева и верификации в некоторых турнирах", - "search.info": "Поиск пользователей по имени Discord или Splatoon 3", "search.noResults": "По запросу '{{query}}' пользователь не найден", "search.pleaseLogIn.header": "Пожалуйста, войдите в аккаунт для поиска пользователя", "search.pleaseLogIn.button": "Войти с помощью Discord", - "seasons": "Сезоны", "seasons.noQ": "Данный пользователь не играл в SendouQ в этом сезоне", + "seasons.noSeasons": "", "seasons.tabs.weapons": "Оружие", "seasons.tabs.stages": "Арены", "seasons.tabs.self": "Я", @@ -67,7 +63,6 @@ "seasons.clickARow": "Нажмите на ряд, чтобы посмотреть на статистику использованного оружия.", "seasons.loading": "Загрузка...", "seasons.matchBeingProcessed": "Данный матч ещё не был обработан", - "builds.sorting.changeButton": "Изменить сортировку", "builds.sorting.header": "Изменить сортировку сборок", "builds.sorting.backToDefaults": "По умолчанию", diff --git a/locales/zh/user.json b/locales/zh/user.json index afe5b7c17..db384ed5c 100644 --- a/locales/zh/user.json +++ b/locales/zh/user.json @@ -46,6 +46,7 @@ "search.pleaseLogIn.button": "", "seasons": "赛季", "seasons.noQ": "", + "seasons.noSeasons": "", "seasons.tabs.weapons": "武器", "seasons.tabs.stages": "地图", "seasons.tabs.self": "自己",