Fix a bunch of TODOs (#2648)
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled

This commit is contained in:
Kalle
2025-11-23 16:34:18 +02:00
committed by GitHub
parent dc84987fe7
commit ef2d3779ec
54 changed files with 236 additions and 330 deletions

View File

@@ -4,6 +4,7 @@
- if you encounter an existing TODO comment assume it is there for a reason and do not remove it
- task is not considered completely until `npm run checks` passes
- normal file structure has constants at the top immediately followed by the main function body of the file. Helpers are used to structure the code and they are at the bottom of the file (then hoisted to the top)
- note: any formatting issue (such as tabs vs. spaces) can be resolved by running the `npm run biome:fix` command
## Commands

View File

@@ -29,7 +29,7 @@ export default function Chart({
const primaryAxis = React.useMemo<
AxisOptions<(typeof options)[number]["data"][number]>
>(
// @ts-expect-error TODO: type this
// @ts-expect-error - some weirdness here but maybe not worth fixing as the whole library needs to be replaced (it is unmaintained/deprecated)
() => ({
getValue: (datum) => datum.primary,
scaleType: xAxis,

View File

@@ -39,7 +39,7 @@ interface WeaponSelectProps<
) => void;
clearable?: Clearable;
includeSubSpecial?: IncludeSubSpecial;
disabledWeaponIds?: Array<MainWeaponId>; // TODO: implement for `AnyWeapon` if needed
disabledWeaponIds?: Array<MainWeaponId>;
testId?: string;
isRequired?: boolean;
/** If set, selection of weapons that user sees when search input is empty allowing for quick select for e.g. previous selections */

View File

@@ -78,7 +78,6 @@ function NotificationContent({
const { t } = useTranslation(["common"]);
const { revalidate, state } = useRevalidator();
// TODO: for some reason this makes "adds a badge owner sending a notification" E2E test flaky, figure out why and fix
useMarkNotificationsAsSeen(unseenIds);
return (

View File

@@ -443,7 +443,11 @@ function Seed() {
>
<div className="stack horizontal md items-end">
<SubmitButton state={fetcher.state}>Seed</SubmitButton>
<SendouSelect label="Variation" name="variation">
<SendouSelect
label="Variation"
name="variation"
defaultSelectedKey="DEFAULT"
>
{SEED_VARIATIONS.map((variation) => (
<SendouSelectItem key={variation} id={variation}>
{variation}

View File

@@ -466,5 +466,4 @@ type TournamentBracket = {
name: string;
};
// TODO: use a better documented type here
type TournamentBracketData = ValueToArray<DataTypes>;

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { Main } from "~/components/Main";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { ARTICLES_MAIN_PAGE, articlePage, navIconUrl } from "~/utils/urls";
import { joinListToNaturalString } from "../../../utils/arrays";
import { metaTags } from "../../../utils/remix";
import { loader } from "../loaders/a.server";
@@ -29,7 +28,7 @@ export const meta: MetaFunction = (args) => {
};
export default function ArticlesMainPage() {
const { t } = useTranslation(["common"]);
const { t, i18n } = useTranslation(["common"]);
const data = useLoaderData<typeof loader>();
return (
@@ -45,10 +44,9 @@ export default function ArticlesMainPage() {
</Link>
<div className="text-xs text-lighter">
{t("common:articles.by", {
author: joinListToNaturalString(
article.authors.map((a) => a.name),
"&",
),
author: new Intl.ListFormat(i18n.language, {
style: "short",
}).format(article.authors.map((a) => a.name)),
})}{" "}
<time>{article.dateString}</time>
</div>

View File

@@ -41,7 +41,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
filters.success && filters.data && filters.data.length > 0;
const builds = await BuildRepository.allByWeaponId(weaponId, {
limit: hasActiveFilters ? BUILDS_PAGE_MAX_BUILDS : limit,
limit: hasActiveFilters ? BUILDS_PAGE_MAX_BUILDS : limit + 1,
sortAbilities: !user?.preferences?.disableBuildAbilitySorting,
});
@@ -56,15 +56,25 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
? filterBuilds({
builds,
filters: filters.data!,
count: limit,
count: limit + 1,
})
: builds;
let hasMoreBuilds = false;
if (filteredBuilds.length > limit) {
filteredBuilds.pop();
if (limit < BUILDS_PAGE_MAX_BUILDS) {
hasMoreBuilds = true;
}
}
return {
weaponId,
weaponName,
builds: filteredBuilds,
limit,
hasMoreBuilds,
slug,
filters: filters.success ? filters.data : [],
};

View File

@@ -346,19 +346,16 @@ export default function WeaponsBuildsPage() {
</div>
) : null}
<BuildCards data={data} />
{data.limit < BUILDS_PAGE_MAX_BUILDS &&
// not considering edge case where there are amount of builds equal to current limit
// TODO: this could be fixed by taking example from the vods page
data.builds.length === data.limit && (
<LinkButton
className="m-0-auto"
size="small"
to={loadMoreLink()}
preventScrollReset
>
{t("common:actions.loadMore")}
</LinkButton>
)}
{data.limit < BUILDS_PAGE_MAX_BUILDS && data.hasMoreBuilds ? (
<LinkButton
className="m-0-auto"
size="small"
to={loadMoreLink()}
preventScrollReset
>
{t("common:actions.loadMore")}
</LinkButton>
) : null}
</Main>
);
}

View File

@@ -2,6 +2,7 @@ import type {
Expression,
ExpressionBuilder,
NotNull,
SqlBool,
Transaction,
} from "kysely";
import { sql } from "kysely";
@@ -38,15 +39,20 @@ import {
import type { CalendarEvent } from "./calendar-types";
import { calendarEventSorter } from "./calendar-utils";
// TODO: convert from raw to using the "exists" function
const hasBadge = sql<number> /* sql */`exists (
select
1
from
"CalendarEventBadge"
where
"CalendarEventBadge"."eventId" = "CalendarEventDate"."eventId"
)`.as("hasBadge");
function hasBadge(eb: ExpressionBuilder<DB, "CalendarEventDate">) {
return eb
.exists(
eb
.selectFrom("CalendarEventBadge")
.select("CalendarEventBadge.eventId")
.whereRef(
"CalendarEventBadge.eventId",
"=",
"CalendarEventDate.eventId",
),
)
.as("hasBadge");
}
const withMapPool = (eb: ExpressionBuilder<DB, "CalendarEvent">) => {
return jsonArrayFrom(
@@ -366,7 +372,7 @@ export async function findById(
)
.innerJoin("User", "CalendarEvent.authorId", "User.id")
.leftJoin("Tournament", "CalendarEvent.tournamentId", "Tournament.id")
.select(({ ref }) => [
.select((eb) => [
"CalendarEvent.name",
"CalendarEvent.description",
"CalendarEvent.discordInviteCode",
@@ -383,8 +389,8 @@ export async function findById(
"User.username",
"User.discordId",
"User.discordAvatar",
hasBadge,
tournamentOrganization(ref("CalendarEvent.organizationId")).as(
hasBadge(eb),
tournamentOrganization(eb.ref("CalendarEvent.organizationId")).as(
"organization",
),
])
@@ -423,7 +429,7 @@ export async function findRecentTournamentsByAuthorId(authorId: number) {
}
function tagsArray(args: {
hasBadge: number;
hasBadge: SqlBool;
tags?: Tables["CalendarEvent"]["tags"];
tournamentId: Tables["CalendarEvent"]["tournamentId"];
}) {

View File

@@ -1,9 +1,8 @@
import { compareTwoTiers } from "~/features/mmr/mmr-utils";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import {
altWeaponIdToId,
mainWeaponIds,
weaponIdToAltId,
weaponIdToBaseWeaponId,
} from "~/modules/in-game-lists/weapon-ids";
import { assertUnreachable } from "~/utils/types";
import type { LFGFilter } from "../lfg-types";
@@ -128,34 +127,14 @@ const checkMatchesSomeUserInPost = (
return false;
};
// TODO: could be written more clearly, fails in some edge cases like if "Hero Shot" was selected it won't find "Octo Shot"
const weaponIdToRelated = (weaponSplId: MainWeaponId) => {
const idsSet = new Set<MainWeaponId>([weaponSplId]);
const result: MainWeaponId[] = [];
const reg = altWeaponIdToId.get(weaponSplId);
if (reg) {
idsSet.add(reg);
}
const alt = weaponIdToAltId.get(weaponSplId);
if (alt) {
for (const id of Array.isArray(alt) ? alt : [alt]) {
idsSet.add(id);
for (const id of mainWeaponIds) {
if (weaponIdToBaseWeaponId(id) === weaponIdToBaseWeaponId(weaponSplId)) {
result.push(id);
}
}
const finalIdsSet = new Set<MainWeaponId>(idsSet);
for (const id of idsSet) {
// alt kits
const maybeId1 = id - 1;
const maybeId2 = id + 1;
for (const maybeId of [maybeId1, maybeId2]) {
if (mainWeaponIds.includes(maybeId as MainWeaponId)) {
finalIdsSet.add(maybeId as MainWeaponId);
}
}
}
return Array.from(finalIdsSet);
return result;
};

View File

@@ -310,35 +310,30 @@ describe("MapList.generate()", () => {
}
});
// TODO: fix flaky
it(
"replenishes the stage id pool with different order",
{ retry: 10 },
() => {
const gen = initGenerator(
new MapPool({
TW: [],
SZ: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
TC: [],
RM: [],
CB: [],
}),
);
const first = gen.next({ amount: 5 }).value.map((m) => m.stageId);
gen.next({ amount: 5 });
const third = gen.next({ amount: 5 }).value.map((m) => m.stageId);
it("replenishes the stage id pool with different order", () => {
const gen = initGenerator(
new MapPool({
TW: [],
SZ: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
TC: [],
RM: [],
CB: [],
}),
);
const first = gen.next({ amount: 5 }).value.map((m) => m.stageId);
gen.next({ amount: 5 });
const third = gen.next({ amount: 5 }).value.map((m) => m.stageId);
let someDifferent = false;
for (let i = 0; i < 5; i++) {
if (first[i] !== third[i]) {
someDifferent = true;
break;
}
let someDifferent = false;
for (let i = 0; i < 5; i++) {
if (first[i] !== third[i]) {
someDifferent = true;
break;
}
}
expect(someDifferent).toBe(true);
},
);
expect(someDifferent).toBe(true);
});
it("should find unique maps when possible (All 4 One #50 bug)", () => {
const mapPool = new MapPool({

View File

@@ -11,7 +11,6 @@ const groupedSkillsStm = sql.prepare(/* sql */ `
"Skill"
left join "GroupMatch" on "GroupMatch"."id" = "Skill"."groupMatchId"
left join "Tournament" on "Tournament"."id" = "Skill"."tournamentId"
-- TODO: support tournament having many start dates
left join "CalendarEvent" on "Tournament"."id" = "CalendarEvent"."tournamentId"
left join "CalendarEventDate" on "CalendarEvent"."id" = "CalendarEventDate"."eventId"
where

View File

@@ -86,8 +86,7 @@ function resolveRelevantKey({
if (!weaponIds.includes(normalizedWeaponId)) continue;
if (damageType !== type) continue;
// @ts-expect-error TODO: fix this (5.5 version)
if (!actualKeys.includes(key)) {
if (!actualKeys.includes(key as Exclude<typeof key, "Default">)) {
throw new Error(
`Invalid damagePriorities (no key in object-dmg.json for the weapon): ${JSON.stringify(
[weaponType, weaponIds, damageType, key],

View File

@@ -1,5 +1,5 @@
import { formatDistance } from "date-fns";
import type { Insertable } from "kysely";
import type { Insertable, NotNull } from "kysely";
import { jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB } from "~/db/tables";
@@ -8,32 +8,9 @@ import { databaseTimestampToDate } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
// TODO: can be made better when $narrowNotNull lands
type FindAllByMonthRow = {
tier: number;
id: number;
text: string;
createdAt: number;
author: {
id: number;
username: string;
discordId: string;
discordAvatar: string | null;
};
suggested: {
id: number;
username: string;
discordId: string;
discordAvatar: string | null;
bio: string | null;
plusTier: number | null;
};
};
// TODO: naming is a bit weird here (suggestion inside suggestions)
export type FindAllByMonthItem = Unwrapped<typeof findAllByMonth>;
export async function findAllByMonth(args: MonthYear) {
const allRows = (await db
const allRows = await db
.selectFrom("PlusSuggestion")
.select(({ eb }) => [
"PlusSuggestion.id",
@@ -61,7 +38,8 @@ export async function findAllByMonth(args: MonthYear) {
.where("PlusSuggestion.month", "=", args.month)
.where("PlusSuggestion.year", "=", args.year)
.orderBy("PlusSuggestion.createdAt", "asc")
.execute()) as FindAllByMonthRow[];
.$narrowType<{ author: NotNull; suggested: NotNull }>()
.execute();
// filter out suggestions that were made in the time period
// between voting ending and people gaining access from the leaderboard
@@ -69,26 +47,26 @@ export async function findAllByMonth(args: MonthYear) {
(r) => !r.suggested.plusTier || r.suggested.plusTier > r.tier,
);
type Row = (typeof rows)[number];
const result: Array<{
suggested: FindAllByMonthRow["suggested"];
tier: FindAllByMonthRow["tier"];
suggestions: Array<{
author: FindAllByMonthRow["author"];
suggested: Row["suggested"];
tier: Row["tier"];
entries: Array<{
author: Row["author"];
createdAtRelative: string;
createdAt: number;
id: FindAllByMonthRow["id"];
text: FindAllByMonthRow["text"];
id: Row["id"];
text: Row["text"];
}>;
}> = [];
for (const row of rows) {
const existing = result.find(
(suggestion) =>
suggestion.tier === row.tier &&
row.suggested.id === suggestion.suggested.id,
(r) => r.tier === row.tier && row.suggested.id === r.suggested.id,
);
const mappedSuggestion = {
const entry = {
id: row.id,
text: row.text,
createdAtRelative: formatDistance(
@@ -100,19 +78,17 @@ export async function findAllByMonth(args: MonthYear) {
author: row.author,
};
if (existing) {
existing.suggestions.push(mappedSuggestion);
existing.entries.push(entry);
} else {
result.push({
tier: row.tier,
suggested: row.suggested,
suggestions: [mappedSuggestion],
entries: [entry],
});
}
}
return result.sort(
(a, b) => b.suggestions[0].createdAt - a.suggestions[0].createdAt,
);
return result.sort((a, b) => b.entries[0].createdAt - a.entries[0].createdAt);
}
export function create(args: Insertable<DB["PlusSuggestion"]>) {

View File

@@ -33,27 +33,25 @@ export const action: ActionFunction = async ({ request }) => {
await PlusSuggestionRepository.findAllByMonth(votingMonthYear);
const suggestionToDelete = suggestions.find((suggestion) =>
suggestion.suggestions.some(
(suggestion) => suggestion.id === data.suggestionId,
),
suggestion.entries.some((entry) => entry.id === data.suggestionId),
);
invariant(suggestionToDelete);
const subSuggestion = suggestionToDelete.suggestions.find(
(suggestion) => suggestion.id === data.suggestionId,
const entryToDelete = suggestionToDelete.entries.find(
(entry) => entry.id === data.suggestionId,
);
invariant(subSuggestion);
invariant(entryToDelete);
errorToastIfFalsy(
canDeleteComment({
user,
author: subSuggestion.author,
author: entryToDelete.author,
suggestionId: data.suggestionId,
suggestions,
}),
"No permissions to delete this comment",
);
const suggestionHasComments = suggestionToDelete.suggestions.length > 1;
const suggestionHasComments = suggestionToDelete.entries.length > 1;
if (
suggestionHasComments &&

View File

@@ -64,8 +64,8 @@ export function isFirstSuggestion({
suggestions,
}: Pick<CanDeleteCommentArgs, "suggestionId" | "suggestions">) {
for (const suggestedUser of Object.values(suggestions).flat()) {
for (const [i, suggestion] of suggestedUser.suggestions.entries()) {
if (suggestion.id !== suggestionId) continue;
for (const [i, entry] of suggestedUser.entries.entries()) {
if (entry.id !== suggestionId) continue;
return i === 0;
}
@@ -84,9 +84,7 @@ function alreadyCommentedByUser({
(suggestion) =>
suggestion.tier === targetPlusTier &&
suggestion.suggested.id === suggested.id &&
suggestion.suggestions.some(
(suggestion) => suggestion.author.id === user?.id,
),
suggestion.entries.some((entry) => entry.author.id === user?.id),
);
}
@@ -121,10 +119,10 @@ function suggestionHasNoOtherComments({
suggestionId,
}: Pick<CanDeleteCommentArgs, "suggestionId" | "suggestions">) {
for (const suggestedUser of Object.values(suggestions).flat()) {
for (const suggestion of suggestedUser.suggestions) {
if (suggestion.id !== suggestionId) continue;
for (const entry of suggestedUser.entries) {
if (entry.id !== suggestionId) continue;
return suggestedUser.suggestions.length === 1;
return suggestedUser.entries.length === 1;
}
}
@@ -171,6 +169,6 @@ function hasUserSuggestedThisMonth({
suggestions,
}: Pick<CanSuggestNewUserArgs, "user" | "suggestions">) {
return suggestions.some(
(suggestion) => suggestion.suggestions[0].author.id === user?.id,
(suggestion) => suggestion.entries[0].author.id === user?.id,
);
}

View File

@@ -258,33 +258,33 @@ export function PlusSuggestionComments({
return (
<details open={defaultOpen} className="w-full">
<summary className="plus__view-comments-action">
Comments ({suggestion.suggestions.length})
Comments ({suggestion.entries.length})
</summary>
<div className="stack sm mt-2">
{suggestion.suggestions.map((suggestion) => {
{suggestion.entries.map((entry) => {
return (
<fieldset key={suggestion.id} className="plus__comment">
<legend>{suggestion.author.username}</legend>
{suggestion.text}
<fieldset key={entry.id} className="plus__comment">
<legend>{entry.author.username}</legend>
{entry.text}
<div className="stack horizontal xs items-center">
<span className="plus__comment-time">
<RelativeTime
timestamp={databaseTimestampToDate(
suggestion.createdAt,
entry.createdAt,
).getTime()}
>
{suggestion.createdAtRelative}
{entry.createdAtRelative}
</RelativeTime>
</span>
{deleteButtonArgs &&
canDeleteComment({
author: suggestion.author,
author: entry.author,
user: deleteButtonArgs.user,
suggestionId: suggestion.id,
suggestionId: entry.id,
suggestions: deleteButtonArgs.suggestions,
}) ? (
<CommentDeleteButton
suggestionId={suggestion.id}
suggestionId={entry.id}
tier={deleteButtonArgs.tier}
suggestedUsername={deleteButtonArgs.suggested.username}
isFirstSuggestion={

View File

@@ -6,7 +6,7 @@ import { SelectFormField } from "~/components/form/SelectFormField";
import { SendouForm } from "~/components/form/SendouForm";
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import { joinListToNaturalString, nullFilledArray } from "~/utils/arrays";
import { nullFilledArray } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import type { loader as scrimsLoader } from "../loaders/scrims.server";
import type { NewRequestFormFields } from "../routes/scrims";
@@ -23,7 +23,7 @@ export function ScrimRequestModal({
post: ScrimPost;
close: () => void;
}) {
const { t } = useTranslation(["scrims"]);
const { t, i18n } = useTranslation(["scrims"]);
const data = useLoaderData<typeof scrimsLoader>();
const { formatTime } = useTimeFormat();
@@ -58,7 +58,9 @@ export function ScrimRequestModal({
}}
>
<div className="font-semi-bold text-lighter italic">
{joinListToNaturalString(post.users.map((u) => u.username))}
{new Intl.ListFormat(i18n.language).format(
post.users.map((u) => u.username),
)}
</div>
{post.text ? (
<div className="text-sm text-lighter italic">{post.text}</div>

View File

@@ -51,7 +51,6 @@ import { useWindowSize } from "~/hooks/useWindowSize";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
import { useHasRole } from "~/modules/permissions/hooks";
import { joinListToNaturalString } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import { animate } from "~/utils/flip";
import invariant from "~/utils/invariant";
@@ -85,9 +84,9 @@ export const meta: MetaFunction = (args) => {
return metaTags({
title: `SendouQ - Match #${data.match.id}`,
description: `${joinListToNaturalString(
description: `${new Intl.ListFormat("en-US").format(
data.groupAlpha.members.map((m) => m.username),
)} vs. ${joinListToNaturalString(
)} vs. ${new Intl.ListFormat("en-US").format(
data.groupBravo.members.map((m) => m.username),
)}`,
location: args.location,

View File

@@ -20,7 +20,6 @@ import type * as Seasons from "~/features/mmr/core/Seasons";
import { useAutoRerender } from "~/hooks/useAutoRerender";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasRole } from "~/modules/permissions/hooks";
import { joinListToNaturalString } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
@@ -254,7 +253,7 @@ function JoinTeamDialog({
role: Tables["GroupMember"]["role"];
}[];
}) {
const { t } = useTranslation(["q"]);
const { t, i18n } = useTranslation(["q"]);
const fetcher = useFetcher();
const owner = members.find((m) => m.role === "OWNER");
@@ -267,7 +266,9 @@ function JoinTeamDialog({
isDismissable
className="text-center"
heading={t("q:front.join.header", {
members: joinListToNaturalString(members.map((m) => m.username)),
members: new Intl.ListFormat(i18n.language).format(
members.map((m) => m.username),
),
})}
>
<fetcher.Form

View File

@@ -14,7 +14,6 @@ import { SubmitButton } from "~/components/SubmitButton";
import { useUser } from "~/features/auth/core/user";
import { usePagination } from "~/hooks/usePagination";
import { useHasRole } from "~/modules/permissions/hooks";
import { joinListToNaturalString } from "~/utils/arrays";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
@@ -50,7 +49,7 @@ export const handle: SendouRouteHandle = {
};
export default function TeamSearchPage() {
const { t } = useTranslation(["team"]);
const { t, i18n } = useTranslation(["team"]);
const [inputValue, setInputValue] = React.useState("");
const data = useLoaderData<typeof loader>();
@@ -143,10 +142,9 @@ export default function TeamSearchPage() {
<div className="team-search__team__members">
{team.members.length === 1
? team.members[0].username
: joinListToNaturalString(
team.members.map((member) => member.username),
"&",
)}
: new Intl.ListFormat(i18n.language, {
style: "short",
}).format(team.members.map((member) => member.username))}
</div>
</div>
</Link>

View File

@@ -82,11 +82,13 @@ export default function TournamentBracketsPage() {
revalidate();
}, [visibility, revalidate, tournament.ctx.isFinalized]);
const teamProgressStatus = tournament.teamMemberOfProgressStatus(user);
const showAddSubsButton =
!tournament.canFinalize(user) &&
!tournament.everyBracketOver &&
tournament.hasStarted &&
tournament.autonomousSubs;
tournament.autonomousSubs &&
teamProgressStatus?.type !== "THANKS_FOR_PLAYING";
const showPrepareMapsButton =
tournament.isOrganizer(user) &&
@@ -217,10 +219,7 @@ export default function TournamentBracketsPage() {
<div className="stack horizontal mb-4 sm justify-between items-center">
{/** TournamentTeamActions more confusing than helpful for leagues, for example might say "Waiting for match..." when previous match was rescheduled */}
{!tournament.isLeagueDivision ? <TournamentTeamActions /> : null}
{showAddSubsButton ? (
// TODO: could also hide this when team is not in any bracket anymore
<AddSubsPopOver />
) : null}
{showAddSubsButton ? <AddSubsPopOver /> : null}
</div>
<div className="stack md">
<div className="stack horizontal sm">

View File

@@ -74,8 +74,6 @@
display: flex;
justify-content: space-between;
padding: var(--s-2);
/* TODO: add fallback from Firefox */
backdrop-filter: blur(5px);
background: rgb(0 0 0 / 40%);
border-start-end-radius: var(--rounded);

View File

@@ -1,5 +1,6 @@
import type { SerializeFrom } from "@remix-run/node";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { LinkButton } from "~/components/elements/Button";
import type { MonthYear } from "~/features/plus-voting/core";
import { useIsMounted } from "~/hooks/useIsMounted";
@@ -14,9 +15,6 @@ interface EventCalendarProps {
fallbackLogoUrl: string;
}
// TODO: i18n
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
export function EventCalendar({
month,
year,
@@ -25,12 +23,20 @@ export function EventCalendar({
}: EventCalendarProps) {
const dates = nullPaddedDatesOfMonth({ month, year });
const isMounted = useIsMounted();
const { i18n } = useTranslation();
const dayHeaders = Array.from({ length: 7 }, (_, i) => {
const date = new Date(2024, 0, 1 + i);
return new Intl.DateTimeFormat(i18n.language, { weekday: "short" }).format(
date,
);
});
return (
<div className="org__calendar__container">
<MonthSelector month={month} year={year} />
<div className="org__calendar">
{DAY_HEADERS.map((day) => (
{dayHeaders.map((day) => (
<div key={day} className="org__calendar__day-header">
{day}
</div>

View File

@@ -8,6 +8,7 @@ import {
errorToastIfFalsy,
parseParams,
parseRequestPayload,
successToast,
} from "~/utils/remix.server";
import { idObject } from "~/utils/zod";
import { updateTeamSeeds } from "../queries/updateTeamSeeds.server";
@@ -32,7 +33,8 @@ export const action: ActionFunction = async ({ request, params }) => {
switch (data._action) {
case "UPDATE_SEEDS": {
updateTeamSeeds({ tournamentId, teamIds: data.seeds });
break;
clearTournamentDataCache(tournamentId);
return successToast("Seeds saved successfully");
}
case "UPDATE_STARTING_BRACKETS": {
const validBracketIdxs =

View File

@@ -18,7 +18,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
throw new Response(null, { status: 404 });
}
// TODO: could be inferred from tournament data (winCounts too)
const sets = tournamentTeamSets({ tournamentTeamId, tournamentId });
return {

View File

@@ -26,7 +26,6 @@ const deleteMemberStm = sql.prepare(/*sql*/ `
and "userId" = @userId
`);
// TODO: if captain leaves don't delete but give captain to someone else
export const joinTeam = sql.transaction(
({
previousTeamId,

View File

@@ -1049,12 +1049,15 @@ function FillRoster({
might result in disqualification.
</div>
) : (
// TODO: proper English for 1v1 "At least 1 members are required to participate. Max roster size is 1"
<div className="tournament__section__warning">
{t("tournament:pre.roster.footer", {
atLeastCount: tournament.minMembersPerTeam,
maxCount: tournament.maxMembersPerTeam,
})}
{tournament.minMembersPerTeam <= 3
? t("tournament:pre.roster.footer.noSubs", {
format: `${tournament.minMembersPerTeam}v${tournament.minMembersPerTeam}`,
})
: t("tournament:pre.roster.footer", {
atLeastCount: tournament.minMembersPerTeam,
maxCount: tournament.maxMembersPerTeam,
})}
</div>
)}
</div>
@@ -1165,7 +1168,6 @@ function DeleteMember({ members }: { members: TournamentDataTeam["members"] }) {
);
}
// TODO: useBlocker to prevent leaving page if made changes without saving
function CounterPickMapPoolPicker() {
const { t } = useTranslation(["common", "game-misc", "tournament"]);
const tournament = useTournament();

View File

@@ -24,7 +24,6 @@ import { SendouDialog } from "~/components/elements/Dialog";
import { SubmitButton } from "~/components/SubmitButton";
import { Table } from "~/components/Table";
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
import { useTimeoutState } from "~/hooks/useTimeoutState";
import invariant from "~/utils/invariant";
import { userResultsPage } from "~/utils/urls";
import { Avatar } from "../../../components/Avatar";
@@ -55,7 +54,7 @@ export default function TournamentSeedsPage() {
}),
);
const teamsSorted = tournament.ctx.teams.sort(
const teamsSorted = [...tournament.ctx.teams].sort(
(a, b) => teamOrder.indexOf(a.id) - teamOrder.indexOf(b.id),
);
@@ -333,20 +332,9 @@ function StartingBracketDialog() {
function SeedAlert({ teamOrder }: { teamOrder: number[] }) {
const tournament = useTournament();
const [teamOrderInDb, setTeamOrderInDb] = React.useState(teamOrder);
const [showSuccess, setShowSuccess] = useTimeoutState(false);
const fetcher = useFetcher();
// TODO: figure out a better way
// biome-ignore lint/correctness/useExhaustiveDependencies: biome migration
React.useEffect(() => {
// TODO: what if error?
if (fetcher.state !== "loading") return;
setTeamOrderInDb(teamOrder);
setShowSuccess(true, { timeout: 3000 });
}, [fetcher.state]);
const teamOrderInDb = tournament.ctx.teams.map((t) => t.id);
const teamOrderChanged = teamOrder.some((id, i) => id !== teamOrderInDb[i]);
return (
@@ -355,26 +343,20 @@ function SeedAlert({ teamOrder }: { teamOrder: number[] }) {
<input type="hidden" name="seeds" value={JSON.stringify(teamOrder)} />
<input type="hidden" name="_action" value="UPDATE_SEEDS" />
<Alert
variation={
teamOrderChanged ? "WARNING" : showSuccess ? "SUCCESS" : "INFO"
}
variation={teamOrderChanged ? "WARNING" : "INFO"}
alertClassName="tournament-bracket__start-bracket-alert"
textClassName="stack horizontal md items-center"
>
{teamOrderChanged
? "You have unchanged changes to seeding"
: showSuccess
? "Seeds saved successfully!"
: "Drag teams to adjust their seeding"}
{(!showSuccess || teamOrderChanged) && (
<SubmitButton
state={fetcher.state}
isDisabled={!teamOrderChanged}
size="small"
>
Save seeds
</SubmitButton>
)}
? "You have unsaved changes to seeding"
: "Drag teams to adjust their seeding"}
<SubmitButton
state={fetcher.state}
isDisabled={!teamOrderChanged}
size="small"
>
Save seeds
</SubmitButton>
</Alert>
</fetcher.Form>
);

View File

@@ -1,33 +0,0 @@
import * as React from "react";
// TODO: fix causes memory leak
/** @link https://stackoverflow.com/a/64983274 */
export const useTimeoutState = <T>(
defaultState: T,
): [
T,
(action: React.SetStateAction<T>, opts?: { timeout: number }) => void,
] => {
const [state, _setState] = React.useState<T>(defaultState);
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<
NodeJS.Timeout | undefined
>();
const setState = React.useCallback(
(action: React.SetStateAction<T>, opts?: { timeout: number }) => {
if (currentTimeoutId != null) {
clearTimeout(currentTimeoutId);
}
_setState(action);
const id = setTimeout(
() => _setState(defaultState),
opts?.timeout ?? 4000,
);
setCurrentTimeoutId(id);
},
[currentTimeoutId, defaultState],
);
return [state, setState];
};

View File

@@ -47,8 +47,8 @@ import { getThemeSession } from "./features/theme/core/session.server";
import { useIsMounted } from "./hooks/useIsMounted";
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
import i18next, { i18nCookie } from "./modules/i18n/i18next.server";
import type { Namespace } from "./modules/i18n/resources.server";
import { IS_E2E_TEST_RUN } from "./utils/e2e";
import { allI18nNamespaces } from "./utils/i18n";
import { isRevalidation, metaTags } from "./utils/remix";
import { SUSPENDED_PAGE } from "./utils/urls";
@@ -147,8 +147,6 @@ function Document({
const navigate = useNavigate();
const locale = data?.locale ?? DEFAULT_LANGUAGE;
// TODO: re-enable after testing if it causes bug where JS is not loading on revisit
// useRevalidateOnRevisit();
useChangeLanguage(locale);
usePreloadTranslation();
useLoadingIndicator();
@@ -248,36 +246,9 @@ function useLoadingIndicator() {
);
}
// TODO: this should be an array if we can figure out how to make Typescript
// enforce that it has every member of keyof CustomTypeOptions["resources"] without duplicating the type manually
export const namespaceJsonsToPreloadObj: Record<Namespace, boolean> = {
common: true,
analyzer: true,
badges: true,
builds: true,
calendar: true,
contributions: true,
faq: true,
"game-misc": true,
gear: true,
user: true,
weapons: true,
scrims: true,
tournament: true,
team: true,
"tier-list-maker": true,
vods: true,
art: true,
q: true,
lfg: true,
org: true,
front: true,
};
const namespaceJsonsToPreload = Object.keys(namespaceJsonsToPreloadObj);
function usePreloadTranslation() {
React.useEffect(() => {
void generalI18next.loadNamespaces(namespaceJsonsToPreload);
void generalI18next.loadNamespaces(allI18nNamespaces());
}, []);
}

View File

@@ -49,8 +49,6 @@ progress {
textarea:not(.plain, [name="text"]):focus-within {
border-color: transparent;
/* TODO: rectangle on Safari */
outline: 2px solid var(--theme);
}
@@ -96,8 +94,6 @@ input:user-invalid {
input:not(.plain, .combobox-input):focus-within {
border-color: transparent;
/* TODO: rectangle on Safari */
outline: 2px solid var(--theme);
}
@@ -191,8 +187,6 @@ select {
border: 2px solid var(--border);
border-radius: var(--rounded-sm);
background: var(--select-background, var(--bg-input));
/* TODO: Get color from CSS var */
background-image: url('data:image/svg+xml;utf8,<svg width="17px" color="rgb(255 255 255 / 55%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /></svg>');
background-position: center right var(--s-3);
background-repeat: no-repeat;
@@ -208,9 +202,7 @@ select:disabled {
transform: initial;
}
/* Temporary solution for issue: https://github.com/sendou-ink/sendou.ink/issues/1141 */
.light select {
/* TODO: Get color from CSS var */
background-image: url('data:image/svg+xml;utf8,<svg width="17px" color="rgb(0 0 0 / 55%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>');
}
@@ -308,8 +300,6 @@ abbr[title] {
.input-container:focus-within {
border-color: transparent;
/* TODO: rectangle on Safari */
outline: 2px solid var(--theme);
}

View File

@@ -31,7 +31,6 @@ export function wrappedAction<T extends z.ZodTypeAny>({
/** Is this action submitted as json (via SendouForm) */
isJsonSubmission = false,
}: {
// TODO: strongly type this
action: (args: ActionFunctionArgs) => any;
isJsonSubmission?: boolean;
}) {
@@ -86,7 +85,6 @@ export function wrappedAction<T extends z.ZodTypeAny>({
export function wrappedLoader<T>({
loader,
}: {
// TODO: strongly type this
loader: (args: LoaderFunctionArgs) => any;
}) {
return async ({

View File

@@ -30,17 +30,6 @@ function at<T>(arr: T[], n: number) {
return arr[n];
}
// TODO: i18n (at least for SendouQ)
export function joinListToNaturalString(arg: string[], lastSeparator = "and") {
if (arg.length === 1) return arg[0];
const list = [...arg];
const last = list.pop();
const commaJoined = list.join(", ");
return last ? `${commaJoined} ${lastSeparator} ${last}` : commaJoined;
}
export function normalizeFormFieldArray(
value: undefined | null | string | string[],
): string[] {

View File

@@ -1,4 +1,37 @@
import type { Namespace } from "~/modules/i18n/resources.server";
import { logger } from "./logger";
import { assertType } from "./types";
// note: cannot get from resources.server.ts directly, because that is a server-only file
const ALL_NAMESPACES = [
"common",
"analyzer",
"badges",
"builds",
"calendar",
"contributions",
"faq",
"game-misc",
"gear",
"user",
"weapons",
"scrims",
"tournament",
"team",
"tier-list-maker",
"vods",
"art",
"q",
"lfg",
"org",
"front",
] as const;
assertType<Namespace, (typeof ALL_NAMESPACES)[number]>();
assertType<(typeof ALL_NAMESPACES)[number], Namespace>();
export function allI18nNamespaces() {
return [...ALL_NAMESPACES];
}
/**
* Returns the localized display name for a given ISO country code using the specified language. If the country code is unknown or the function fails for othe reason, returns the country code itself as a fallback.

View File

@@ -193,8 +193,6 @@ export function canAccessLohiEndpoint(request: Request) {
return request.headers.get(LOHI_TOKEN_HEADER_NAME) === process.env.LOHI_TOKEN;
}
// TODO: investigate better solution to toasts when middlewares land (current one has a problem of clearing search params)
export function errorToastRedirect(message: string) {
return redirect(`?__error=${message}`);
}

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mit hold foretrækker ikke at være vært for rummet",
"pre.roster.header": "Udfyld holdmedlemslisten",
"pre.roster.footer": "Mindst {{atLeastCount}} holdmedlemmer kræves for at deltage. Der kan maks være {{maxCount}} på holdet",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Tilføj personer, som du har spillet med",
"pre.roster.delete.button": "Fjern medlem",
"pre.roster.delete.header": "Medlem der fjernes",
@@ -91,7 +92,7 @@
"team.seed": "Seed",
"team.seed.footer": "Ud af {{count}}",
"team.placement": "Placering",
"team.placement.footer": "Main/UG",
"team.placement.footer": "",
"bracket.waiting": "Her vil turneringsplanen blive vist, så snart{{count}} hold har registreret sig",
"bracket.waiting.checkin": "Her vil turneringsplanen blive vist, så snart{{count}} hold er tjekket ind",
"bracket.wip": "Denne turneringsplan er en forhåndsvisning og kan blive ændret",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mein Team zieht es vor, keine Räume zu hosten",
"pre.roster.header": "Roster füllen",
"pre.roster.footer": "Mindestens {{atLeastCount}} Teammitglieder sind zum Spielen erforderlich. Maximale Rostergröße ist {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Spieler hinzufügen, mit denen du gespielt hast",
"pre.roster.delete.button": "Mitglied löschen",
"pre.roster.delete.header": "Zu entfernendes Mitglied",
@@ -91,7 +92,7 @@
"team.seed": "Seed",
"team.seed.footer": "von {{count}}",
"team.placement": "Platzierung",
"team.placement.footer": "Main/UG",
"team.placement.footer": "",
"bracket.waiting": "Bracket wird hier angezeigt, sobald mindestens {{count}} Teams registriert sind",
"bracket.waiting.checkin": "Bracket wird hier angezeigt, sobald mindestens {{count}} Teams eingecheckt sind",
"bracket.wip": "Dieses Bracket ist eine Vorschau und kann sich ändern",

View File

@@ -26,7 +26,8 @@
"pre.info.unregister.confirm": "Unregister from the tournament and delete team info?",
"pre.info.noHost": "My team prefers not to host rooms",
"pre.roster.header": "Fill roster",
"pre.roster.footer": "At least {{atLeastCount}} members are required to participate. Max roster size is {{maxCount}}",
"pre.roster.footer": "At least {{atLeastCount}} members are required to participate. Max roster size is {{maxCount}}.",
"pre.roster.footer.noSubs": "Format is {{format}}. No subs allowed.",
"pre.roster.addTrusted.header": "Add people you have played with",
"pre.roster.delete.button": "Delete member",
"pre.roster.delete.header": "Member to delete",
@@ -91,7 +92,7 @@
"team.seed": "Seed",
"team.seed.footer": "out of {{count}}",
"team.placement": "Placement",
"team.placement.footer": "Main/UG",
"team.placement.footer": "Overall/Bracket",
"bracket.waiting": "Bracket will be shown here when at least {{count}} teams have registered",
"bracket.waiting.checkin": "Bracket will be shown here when at least {{count}} teams have checked in",
"bracket.wip": "This bracket is a preview and subject to change",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mi equipo prefiere no ser a cargo de salas",
"pre.roster.header": "Llenar equipo",
"pre.roster.footer": "Se requieren al menos {{atLeastCount}} miembros para participar. Cantidad máxima son {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Agregar personas con quienes has jugado",
"pre.roster.delete.button": "Borrar miembro",
"pre.roster.delete.header": "Miembro que quieres borrar",
@@ -93,7 +94,7 @@
"team.seed": "Colocado",
"team.seed.footer": "de {{count}}",
"team.placement": "Colocación",
"team.placement.footer": "Main/UG",
"team.placement.footer": "",
"bracket.waiting": "Cuadro se muestra aquí cuando al menos {{count}} equipos sean registrados",
"bracket.waiting.checkin": "Cuadro se muestra aquí cuando al menos {{count}} equipos se hagan check-in",
"bracket.wip": "Este cuadro es temporal y puede cambiar",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mi equipo prefiere no ser a cargo de salas",
"pre.roster.header": "Llenar equipo",
"pre.roster.footer": "Se requieren al menos {{atLeastCount}} miembros para participar. Cantidad máxima son {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Agregar personas con quienes has jugado",
"pre.roster.delete.button": "Borrar miembro",
"pre.roster.delete.header": "Miembro que quieres borrar",
@@ -93,7 +94,7 @@
"team.seed": "Colocado",
"team.seed.footer": "de {{count}}",
"team.placement": "Colocación",
"team.placement.footer": "Ppal./",
"team.placement.footer": "",
"bracket.waiting": "Cuadro se muestra aquí cuando al menos {{count}} equipos sean registrados",
"bracket.waiting.checkin": "Cuadro se muestra aquí cuando al menos {{count}} equipos se hagan check-in",
"bracket.wip": "Este cuadro es temporal y puede cambiar",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mon équipe préfère ne par héberger",
"pre.roster.header": "Remplir la liste",
"pre.roster.footer": "Au moins {{atLeastCount}} membres sont requis pour participer. La taille maximum est de {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Ajoutez des personnes avec lesquelles vous avez joué",
"pre.roster.delete.button": "Effacer membre",
"pre.roster.delete.header": "Membre à effacer",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Mon équipe préfère ne par héberger",
"pre.roster.header": "Remplir la liste",
"pre.roster.footer": "Au moins {{atLeastCount}} membres sont requis pour participer. La taille maximum est de {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Ajoutez des personnes avec lesquelles vous avez joué",
"pre.roster.delete.button": "Effacer membre",
"pre.roster.delete.header": "Membre à effacer",
@@ -93,7 +94,7 @@
"team.seed": "Seed",
"team.seed.footer": "sur {{count}}",
"team.placement": "Placement",
"team.placement.footer": "Tournois/Bracket",
"team.placement.footer": "",
"bracket.waiting": "Le bracket sera affiché ici quand au moins {{count}} équipes seront inscrites",
"bracket.waiting.checkin": "Le bracket sera affiché ici lorsqu'au moins {{count}} équipes se seront enregistrées",
"bracket.wip": "Ce bracket est un aperçu et sujet à changement",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "הצוות שלי מעדיף לא לארח חדרים",
"pre.roster.header": "מלא צוות",
"pre.roster.footer": "לפחות {{atLeastCount}} חברי צוות נדרשים כדי להשתתף. גודל הצוות המרבי הוא {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "הוסיפו אנשים ששיחקתם איתם",
"pre.roster.delete.button": "מחקו חבר צוות",
"pre.roster.delete.header": "חבר צוות למחיקה",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Il mio team preferisce non hostare le stanze",
"pre.roster.header": "Riempi roster",
"pre.roster.footer": "Sono necessari almeno {{atLeastCount}} membri per partecipare. La dimensione massima del roster è {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Aggiungi persone con cui hai giocato",
"pre.roster.delete.button": "Elimina membro",
"pre.roster.delete.header": "Membro da eliminare",
@@ -93,7 +94,7 @@
"team.seed": "Seed",
"team.seed.footer": "su {{count}}",
"team.placement": "Posizione",
"team.placement.footer": "Main/UG",
"team.placement.footer": "",
"bracket.waiting": "Il bracket verrà mostrato qui una volta che {{count}} team si saranno iscritti",
"bracket.waiting.checkin": "Il bracket verrà mostrato qui una volta che {{count}} team avranno completato il check-in",
"bracket.wip": "Questo bracket è un anteprima ed è soggetto a cambiamenti",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "部屋作成はできれば遠慮したい",
"pre.roster.header": "参加プレイヤーを登録",
"pre.roster.footer": "少なくとも {{atLeastCount}} 人の参加が必要です。最大メンバー数は {{maxCount}} です。",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "一緒にプレイしたことがあるプレイヤーを追加する",
"pre.roster.delete.button": "メンバーを削除する",
"pre.roster.delete.header": "削除するメンバー",
@@ -87,7 +88,7 @@
"team.seed": "シード",
"team.seed.footer": "{{count}} シード中",
"team.placement": "順位",
"team.placement.footer": "メイン/UG",
"team.placement.footer": "",
"bracket.waiting": "ブラケットは、少なくとも {{count}} チームが登録した時点で表示されます",
"bracket.waiting.checkin": "ブラケットは最低{{count}}チームがチェックインしてから表示されるよ",
"bracket.wip": "このブラケットはまだプレビューで、変更される可能性があります",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "",
"pre.roster.header": "",
"pre.roster.footer": "",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "",
"pre.roster.delete.button": "",
"pre.roster.delete.header": "",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "",
"pre.roster.header": "",
"pre.roster.footer": "",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "",
"pre.roster.delete.button": "",
"pre.roster.delete.header": "",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "",
"pre.roster.header": "",
"pre.roster.footer": "",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "",
"pre.roster.delete.button": "",
"pre.roster.delete.header": "",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Meu time prefere não hospedar salas",
"pre.roster.header": "Preencher lista",
"pre.roster.footer": "Pelo menos {{atLeastCount}} membros são necessários para participar. O número máximo da lista de participantes é de {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Adicionar pessoas com que você jogou",
"pre.roster.delete.button": "Excluir membro",
"pre.roster.delete.header": "Membro a ser excluído",
@@ -93,7 +94,7 @@
"team.seed": "Semente",
"team.seed.footer": "dentre {{count}}",
"team.placement": "Placar",
"team.placement.footer": "Principal/UB",
"team.placement.footer": "",
"bracket.waiting": "O bracket será mostrado aqui quando ao menos {{count}} times estiverem registrados",
"bracket.waiting.checkin": "O bracket será mostrado aqui quando pelo menos {{count}} times tiverem feito o check-in",
"bracket.wip": "Esse bracket é uma prévia e poderá mudar",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "Моя команда предпочитает не организовывать игры",
"pre.roster.header": "Заполните состав",
"pre.roster.footer": "Необходимый минимум игроков для данного турнира: {{atLeastCount}}. Максимальное количество игроков в составе: {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "Добавить людей, с которыми вы играли",
"pre.roster.delete.button": "Удалить участника",
"pre.roster.delete.header": "Участник для удаления",
@@ -95,7 +96,7 @@
"team.seed": "Семя",
"team.seed.footer": "из {{count}}",
"team.placement": "Место",
"team.placement.footer": "Главная/Подземная",
"team.placement.footer": "",
"bracket.waiting": "Сетка будет показана как только {{count}} команд зарегистрируется",
"bracket.waiting.checkin": "Сетка будет показана как только {{count}} команд пройдут чек-ин",
"bracket.wip": "Данная сетка является предварительной и может быть изменена.",

View File

@@ -27,6 +27,7 @@
"pre.info.noHost": "我的队伍倾向于不当房主",
"pre.roster.header": "填写阵容",
"pre.roster.footer": "需要至少 {{atLeastCount}} 名成员,成员数量上限为 {{maxCount}}",
"pre.roster.footer.noSubs": "",
"pre.roster.addTrusted.header": "添加与您游玩过的玩家",
"pre.roster.delete.button": "删除成员",
"pre.roster.delete.header": "要删除的成员",
@@ -87,7 +88,7 @@
"team.seed": "种子",
"team.seed.footer": "总共 {{count}} 队",
"team.placement": "排名",
"team.placement.footer": "Main/UG",
"team.placement.footer": "",
"bracket.waiting": "至少 {{count}} 支队伍报名后,对战表将显示在这里",
"bracket.waiting.checkin": "对战表会在至少 {{count}} 支队伍签到后显示",
"bracket.wip": "此对战表为可调整的预览",

View File

@@ -10,7 +10,6 @@ export function up(db) {
`create unique index calendar_event_custom_url_unique on "CalendarEvent"("customUrl")`,
).run();
// TODO: these should be FK's
db.prepare(`alter table "MapPoolMap" add "tournamentTeamId" integer`).run();
db.prepare(
`alter table "MapPoolMap" add "tieBreakerCalendarEventId" integer`,