From ef2d3779ec1373075a46e31d3ee3a7ba75bd1f1f Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:34:18 +0200 Subject: [PATCH] Fix a bunch of TODOs (#2648) --- AGENTS.md | 1 + app/components/Chart.tsx | 2 +- app/components/WeaponSelect.tsx | 2 +- app/components/layout/NotificationPopover.tsx | 1 - app/features/admin/routes/admin.tsx | 6 +- app/features/api-public/schema.ts | 1 - app/features/articles/routes/a.tsx | 10 ++-- .../builds/loaders/builds.$slug.server.ts | 14 ++++- app/features/builds/routes/builds.$slug.tsx | 23 ++++---- .../calendar/CalendarRepository.server.ts | 32 +++++----- app/features/lfg/core/filtering.ts | 33 ++--------- .../map-list-generator/core/MapList.test.ts | 47 +++++++-------- .../queries/seasonAllMMRByUserId.server.ts | 1 - .../core/objectDamage.ts | 3 +- .../PlusSuggestionRepository.server.ts | 58 ++++++------------- .../actions/plus.suggestions.server.ts | 14 ++--- .../plus-suggestions-utils.ts | 16 +++-- .../routes/plus.suggestions.tsx | 20 +++---- .../scrims/components/ScrimRequestModal.tsx | 8 ++- .../sendouq-match/routes/q.match.$id.tsx | 5 +- app/features/sendouq/routes/q.tsx | 7 ++- app/features/team/routes/t.tsx | 10 ++-- .../routes/to.$id.brackets.tsx | 9 ++- .../tournament-bracket/tournament-bracket.css | 2 - .../components/EventCalendar.tsx | 14 +++-- .../tournament/actions/to.$id.seeds.server.ts | 4 +- .../loaders/to.$id.teams.$tid.server.ts | 1 - .../queries/joinLeaveTeam.server.ts | 1 - .../tournament/routes/to.$id.register.tsx | 14 +++-- .../tournament/routes/to.$id.seeds.tsx | 42 ++++---------- app/hooks/useTimeoutState.ts | 33 ----------- app/root.tsx | 33 +---------- app/styles/common.css | 10 ---- app/utils/Test.ts | 2 - app/utils/arrays.ts | 11 ---- app/utils/i18n.ts | 33 +++++++++++ app/utils/remix.server.ts | 2 - locales/da/tournament.json | 3 +- locales/de/tournament.json | 3 +- locales/en/tournament.json | 5 +- locales/es-ES/tournament.json | 3 +- locales/es-US/tournament.json | 3 +- locales/fr-CA/tournament.json | 1 + locales/fr-EU/tournament.json | 3 +- locales/he/tournament.json | 1 + locales/it/tournament.json | 3 +- locales/ja/tournament.json | 3 +- locales/ko/tournament.json | 1 + locales/nl/tournament.json | 1 + locales/pl/tournament.json | 1 + locales/pt-BR/tournament.json | 3 +- locales/ru/tournament.json | 3 +- locales/zh/tournament.json | 3 +- migrations/012-to-tools.js | 1 - 54 files changed, 236 insertions(+), 330 deletions(-) delete mode 100644 app/hooks/useTimeoutState.ts diff --git a/AGENTS.md b/AGENTS.md index f4bc88d8b..2d6b86eb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/app/components/Chart.tsx b/app/components/Chart.tsx index d3fe5d998..c791ba953 100644 --- a/app/components/Chart.tsx +++ b/app/components/Chart.tsx @@ -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, diff --git a/app/components/WeaponSelect.tsx b/app/components/WeaponSelect.tsx index bfebc2ec4..2880cd049 100644 --- a/app/components/WeaponSelect.tsx +++ b/app/components/WeaponSelect.tsx @@ -39,7 +39,7 @@ interface WeaponSelectProps< ) => void; clearable?: Clearable; includeSubSpecial?: IncludeSubSpecial; - disabledWeaponIds?: Array; // TODO: implement for `AnyWeapon` if needed + disabledWeaponIds?: Array; 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 */ diff --git a/app/components/layout/NotificationPopover.tsx b/app/components/layout/NotificationPopover.tsx index 9af28fcc1..d6462cdee 100644 --- a/app/components/layout/NotificationPopover.tsx +++ b/app/components/layout/NotificationPopover.tsx @@ -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 ( diff --git a/app/features/admin/routes/admin.tsx b/app/features/admin/routes/admin.tsx index 8fbf7c1f8..f987df6dc 100644 --- a/app/features/admin/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -443,7 +443,11 @@ function Seed() { >
Seed - + {SEED_VARIATIONS.map((variation) => ( {variation} diff --git a/app/features/api-public/schema.ts b/app/features/api-public/schema.ts index be109ede9..8678c3a1d 100644 --- a/app/features/api-public/schema.ts +++ b/app/features/api-public/schema.ts @@ -466,5 +466,4 @@ type TournamentBracket = { name: string; }; -// TODO: use a better documented type here type TournamentBracketData = ValueToArray; diff --git a/app/features/articles/routes/a.tsx b/app/features/articles/routes/a.tsx index 8d7905c74..cdd517421 100644 --- a/app/features/articles/routes/a.tsx +++ b/app/features/articles/routes/a.tsx @@ -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(); return ( @@ -45,10 +44,9 @@ export default function ArticlesMainPage() {
{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)), })}{" "} •
diff --git a/app/features/builds/loaders/builds.$slug.server.ts b/app/features/builds/loaders/builds.$slug.server.ts index 23649e07e..3dce645f6 100644 --- a/app/features/builds/loaders/builds.$slug.server.ts +++ b/app/features/builds/loaders/builds.$slug.server.ts @@ -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 : [], }; diff --git a/app/features/builds/routes/builds.$slug.tsx b/app/features/builds/routes/builds.$slug.tsx index c7c2d05db..a8b3286d7 100644 --- a/app/features/builds/routes/builds.$slug.tsx +++ b/app/features/builds/routes/builds.$slug.tsx @@ -346,19 +346,16 @@ export default function WeaponsBuildsPage() {
) : null} - {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 && ( - - {t("common:actions.loadMore")} - - )} + {data.limit < BUILDS_PAGE_MAX_BUILDS && data.hasMoreBuilds ? ( + + {t("common:actions.loadMore")} + + ) : null} ); } diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts index af2bc5260..dff57b081 100644 --- a/app/features/calendar/CalendarRepository.server.ts +++ b/app/features/calendar/CalendarRepository.server.ts @@ -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 /* sql */`exists ( - select - 1 - from - "CalendarEventBadge" - where - "CalendarEventBadge"."eventId" = "CalendarEventDate"."eventId" -)`.as("hasBadge"); +function hasBadge(eb: ExpressionBuilder) { + return eb + .exists( + eb + .selectFrom("CalendarEventBadge") + .select("CalendarEventBadge.eventId") + .whereRef( + "CalendarEventBadge.eventId", + "=", + "CalendarEventDate.eventId", + ), + ) + .as("hasBadge"); +} const withMapPool = (eb: ExpressionBuilder) => { 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"]; }) { diff --git a/app/features/lfg/core/filtering.ts b/app/features/lfg/core/filtering.ts index 94753e32d..10148ef9c 100644 --- a/app/features/lfg/core/filtering.ts +++ b/app/features/lfg/core/filtering.ts @@ -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([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(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; }; diff --git a/app/features/map-list-generator/core/MapList.test.ts b/app/features/map-list-generator/core/MapList.test.ts index 7c86f696f..ecd647426 100644 --- a/app/features/map-list-generator/core/MapList.test.ts +++ b/app/features/map-list-generator/core/MapList.test.ts @@ -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({ diff --git a/app/features/mmr/queries/seasonAllMMRByUserId.server.ts b/app/features/mmr/queries/seasonAllMMRByUserId.server.ts index 78281730a..e609104a0 100644 --- a/app/features/mmr/queries/seasonAllMMRByUserId.server.ts +++ b/app/features/mmr/queries/seasonAllMMRByUserId.server.ts @@ -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 diff --git a/app/features/object-damage-calculator/core/objectDamage.ts b/app/features/object-damage-calculator/core/objectDamage.ts index a6029e4b9..91edfd417 100644 --- a/app/features/object-damage-calculator/core/objectDamage.ts +++ b/app/features/object-damage-calculator/core/objectDamage.ts @@ -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)) { throw new Error( `Invalid damagePriorities (no key in object-dmg.json for the weapon): ${JSON.stringify( [weaponType, weaponIds, damageType, key], diff --git a/app/features/plus-suggestions/PlusSuggestionRepository.server.ts b/app/features/plus-suggestions/PlusSuggestionRepository.server.ts index 43a171cc8..d7bafed2a 100644 --- a/app/features/plus-suggestions/PlusSuggestionRepository.server.ts +++ b/app/features/plus-suggestions/PlusSuggestionRepository.server.ts @@ -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; 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) { diff --git a/app/features/plus-suggestions/actions/plus.suggestions.server.ts b/app/features/plus-suggestions/actions/plus.suggestions.server.ts index 40772d87c..6a174a41f 100644 --- a/app/features/plus-suggestions/actions/plus.suggestions.server.ts +++ b/app/features/plus-suggestions/actions/plus.suggestions.server.ts @@ -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 && diff --git a/app/features/plus-suggestions/plus-suggestions-utils.ts b/app/features/plus-suggestions/plus-suggestions-utils.ts index 203540ba3..b2f498b19 100644 --- a/app/features/plus-suggestions/plus-suggestions-utils.ts +++ b/app/features/plus-suggestions/plus-suggestions-utils.ts @@ -64,8 +64,8 @@ export function isFirstSuggestion({ suggestions, }: Pick) { 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) { 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) { return suggestions.some( - (suggestion) => suggestion.suggestions[0].author.id === user?.id, + (suggestion) => suggestion.entries[0].author.id === user?.id, ); } diff --git a/app/features/plus-suggestions/routes/plus.suggestions.tsx b/app/features/plus-suggestions/routes/plus.suggestions.tsx index 92189658c..e53391823 100644 --- a/app/features/plus-suggestions/routes/plus.suggestions.tsx +++ b/app/features/plus-suggestions/routes/plus.suggestions.tsx @@ -258,33 +258,33 @@ export function PlusSuggestionComments({ return (
- Comments ({suggestion.suggestions.length}) + Comments ({suggestion.entries.length})
- {suggestion.suggestions.map((suggestion) => { + {suggestion.entries.map((entry) => { return ( -
- {suggestion.author.username} - {suggestion.text} +
+ {entry.author.username} + {entry.text}
- {suggestion.createdAtRelative} + {entry.createdAtRelative} {deleteButtonArgs && canDeleteComment({ - author: suggestion.author, + author: entry.author, user: deleteButtonArgs.user, - suggestionId: suggestion.id, + suggestionId: entry.id, suggestions: deleteButtonArgs.suggestions, }) ? ( void; }) { - const { t } = useTranslation(["scrims"]); + const { t, i18n } = useTranslation(["scrims"]); const data = useLoaderData(); const { formatTime } = useTimeFormat(); @@ -58,7 +58,9 @@ export function ScrimRequestModal({ }} >
- {joinListToNaturalString(post.users.map((u) => u.username))} + {new Intl.ListFormat(i18n.language).format( + post.users.map((u) => u.username), + )}
{post.text ? (
{post.text}
diff --git a/app/features/sendouq-match/routes/q.match.$id.tsx b/app/features/sendouq-match/routes/q.match.$id.tsx index ef30d5aed..b70bc5f21 100644 --- a/app/features/sendouq-match/routes/q.match.$id.tsx +++ b/app/features/sendouq-match/routes/q.match.$id.tsx @@ -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, diff --git a/app/features/sendouq/routes/q.tsx b/app/features/sendouq/routes/q.tsx index d36d9353e..4d0dc8548 100644 --- a/app/features/sendouq/routes/q.tsx +++ b/app/features/sendouq/routes/q.tsx @@ -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), + ), })} > (); @@ -143,10 +142,9 @@ export default function TeamSearchPage() {
{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))}
diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx index da11418ce..714749598 100644 --- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx +++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx @@ -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() {
{/** TournamentTeamActions more confusing than helpful for leagues, for example might say "Waiting for match..." when previous match was rescheduled */} {!tournament.isLeagueDivision ? : null} - {showAddSubsButton ? ( - // TODO: could also hide this when team is not in any bracket anymore - - ) : null} + {showAddSubsButton ? : null}
diff --git a/app/features/tournament-bracket/tournament-bracket.css b/app/features/tournament-bracket/tournament-bracket.css index 5064a351d..6c7c21a0f 100644 --- a/app/features/tournament-bracket/tournament-bracket.css +++ b/app/features/tournament-bracket/tournament-bracket.css @@ -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); diff --git a/app/features/tournament-organization/components/EventCalendar.tsx b/app/features/tournament-organization/components/EventCalendar.tsx index c7c26341e..512b1cc05 100644 --- a/app/features/tournament-organization/components/EventCalendar.tsx +++ b/app/features/tournament-organization/components/EventCalendar.tsx @@ -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 (
- {DAY_HEADERS.map((day) => ( + {dayHeaders.map((day) => (
{day}
diff --git a/app/features/tournament/actions/to.$id.seeds.server.ts b/app/features/tournament/actions/to.$id.seeds.server.ts index 92692f842..1d7f3da7f 100644 --- a/app/features/tournament/actions/to.$id.seeds.server.ts +++ b/app/features/tournament/actions/to.$id.seeds.server.ts @@ -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 = diff --git a/app/features/tournament/loaders/to.$id.teams.$tid.server.ts b/app/features/tournament/loaders/to.$id.teams.$tid.server.ts index bd909aba9..ac54bf866 100644 --- a/app/features/tournament/loaders/to.$id.teams.$tid.server.ts +++ b/app/features/tournament/loaders/to.$id.teams.$tid.server.ts @@ -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 { diff --git a/app/features/tournament/queries/joinLeaveTeam.server.ts b/app/features/tournament/queries/joinLeaveTeam.server.ts index c9482188a..83b87107a 100644 --- a/app/features/tournament/queries/joinLeaveTeam.server.ts +++ b/app/features/tournament/queries/joinLeaveTeam.server.ts @@ -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, diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index a660ac8e9..dfaa640c5 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -1049,12 +1049,15 @@ function FillRoster({ might result in disqualification.
) : ( - // TODO: proper English for 1v1 "At least 1 members are required to participate. Max roster size is 1"
- {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, + })}
)}
@@ -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(); diff --git a/app/features/tournament/routes/to.$id.seeds.tsx b/app/features/tournament/routes/to.$id.seeds.tsx index 5c011c8ad..7eff713ef 100644 --- a/app/features/tournament/routes/to.$id.seeds.tsx +++ b/app/features/tournament/routes/to.$id.seeds.tsx @@ -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[] }) { {teamOrderChanged - ? "You have unchanged changes to seeding" - : showSuccess - ? "Seeds saved successfully!" - : "Drag teams to adjust their seeding"} - {(!showSuccess || teamOrderChanged) && ( - - Save seeds - - )} + ? "You have unsaved changes to seeding" + : "Drag teams to adjust their seeding"} + + Save seeds + ); diff --git a/app/hooks/useTimeoutState.ts b/app/hooks/useTimeoutState.ts deleted file mode 100644 index c3fb2a2f3..000000000 --- a/app/hooks/useTimeoutState.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from "react"; - -// TODO: fix causes memory leak -/** @link https://stackoverflow.com/a/64983274 */ -export const useTimeoutState = ( - defaultState: T, -): [ - T, - (action: React.SetStateAction, opts?: { timeout: number }) => void, -] => { - const [state, _setState] = React.useState(defaultState); - const [currentTimeoutId, setCurrentTimeoutId] = React.useState< - NodeJS.Timeout | undefined - >(); - - const setState = React.useCallback( - (action: React.SetStateAction, 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]; -}; diff --git a/app/root.tsx b/app/root.tsx index 576b30812..a53e2d826 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -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 = { - 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()); }, []); } diff --git a/app/styles/common.css b/app/styles/common.css index 02f2719a7..dd9ac2467 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -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,'); 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,'); } @@ -308,8 +300,6 @@ abbr[title] { .input-container:focus-within { border-color: transparent; - - /* TODO: rectangle on Safari */ outline: 2px solid var(--theme); } diff --git a/app/utils/Test.ts b/app/utils/Test.ts index 08bff7842..a8a44c47f 100644 --- a/app/utils/Test.ts +++ b/app/utils/Test.ts @@ -31,7 +31,6 @@ export function wrappedAction({ /** 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({ export function wrappedLoader({ loader, }: { - // TODO: strongly type this loader: (args: LoaderFunctionArgs) => any; }) { return async ({ diff --git a/app/utils/arrays.ts b/app/utils/arrays.ts index 990972e3f..ec6731ddb 100644 --- a/app/utils/arrays.ts +++ b/app/utils/arrays.ts @@ -30,17 +30,6 @@ function at(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[] { diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts index 1e4535b9f..047a2b34f 100644 --- a/app/utils/i18n.ts +++ b/app/utils/i18n.ts @@ -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(); +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. diff --git a/app/utils/remix.server.ts b/app/utils/remix.server.ts index 12f6c8039..75e37256b 100644 --- a/app/utils/remix.server.ts +++ b/app/utils/remix.server.ts @@ -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}`); } diff --git a/locales/da/tournament.json b/locales/da/tournament.json index cefae76af..51f4008c5 100644 --- a/locales/da/tournament.json +++ b/locales/da/tournament.json @@ -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", diff --git a/locales/de/tournament.json b/locales/de/tournament.json index aa29ba48c..9534012d6 100644 --- a/locales/de/tournament.json +++ b/locales/de/tournament.json @@ -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", diff --git a/locales/en/tournament.json b/locales/en/tournament.json index a851a2e22..b893b5ab8 100644 --- a/locales/en/tournament.json +++ b/locales/en/tournament.json @@ -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", diff --git a/locales/es-ES/tournament.json b/locales/es-ES/tournament.json index 59624adc9..f10bb0fd9 100644 --- a/locales/es-ES/tournament.json +++ b/locales/es-ES/tournament.json @@ -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", diff --git a/locales/es-US/tournament.json b/locales/es-US/tournament.json index b9e965ec0..a80d26b2a 100644 --- a/locales/es-US/tournament.json +++ b/locales/es-US/tournament.json @@ -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", diff --git a/locales/fr-CA/tournament.json b/locales/fr-CA/tournament.json index 515000694..5f188161b 100644 --- a/locales/fr-CA/tournament.json +++ b/locales/fr-CA/tournament.json @@ -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", diff --git a/locales/fr-EU/tournament.json b/locales/fr-EU/tournament.json index 2197620a0..8cf9d2b82 100644 --- a/locales/fr-EU/tournament.json +++ b/locales/fr-EU/tournament.json @@ -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", diff --git a/locales/he/tournament.json b/locales/he/tournament.json index 4d416d9be..7bff381e9 100644 --- a/locales/he/tournament.json +++ b/locales/he/tournament.json @@ -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": "חבר צוות למחיקה", diff --git a/locales/it/tournament.json b/locales/it/tournament.json index f7d03f423..9ef94e7e0 100644 --- a/locales/it/tournament.json +++ b/locales/it/tournament.json @@ -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", diff --git a/locales/ja/tournament.json b/locales/ja/tournament.json index 4dff91676..29ba571be 100644 --- a/locales/ja/tournament.json +++ b/locales/ja/tournament.json @@ -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": "このブラケットはまだプレビューで、変更される可能性があります", diff --git a/locales/ko/tournament.json b/locales/ko/tournament.json index 3d23b90c9..bdfdd0db1 100644 --- a/locales/ko/tournament.json +++ b/locales/ko/tournament.json @@ -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": "", diff --git a/locales/nl/tournament.json b/locales/nl/tournament.json index b5f208409..d9770e6d7 100644 --- a/locales/nl/tournament.json +++ b/locales/nl/tournament.json @@ -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": "", diff --git a/locales/pl/tournament.json b/locales/pl/tournament.json index 79b067677..72f322eae 100644 --- a/locales/pl/tournament.json +++ b/locales/pl/tournament.json @@ -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": "", diff --git a/locales/pt-BR/tournament.json b/locales/pt-BR/tournament.json index 070847e6e..cbc2d256d 100644 --- a/locales/pt-BR/tournament.json +++ b/locales/pt-BR/tournament.json @@ -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", diff --git a/locales/ru/tournament.json b/locales/ru/tournament.json index 3a09150c9..43efb3c81 100644 --- a/locales/ru/tournament.json +++ b/locales/ru/tournament.json @@ -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": "Данная сетка является предварительной и может быть изменена.", diff --git a/locales/zh/tournament.json b/locales/zh/tournament.json index e5ecb7d26..a63f5e44a 100644 --- a/locales/zh/tournament.json +++ b/locales/zh/tournament.json @@ -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": "此对战表为可调整的预览", diff --git a/migrations/012-to-tools.js b/migrations/012-to-tools.js index cf8fb3a5f..c8fa0d2f0 100644 --- a/migrations/012-to-tools.js +++ b/migrations/012-to-tools.js @@ -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`,