diff --git a/AGENTS.md b/AGENTS.md index 370ec59d0..ad6c4950f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ - for any CSS variable used, make sure it is defined either locally or in the `vars.css` file - for simple styling, prefer [utility classes](./app/styles/utils.css) over creating a new class - use CSS nesting with the `&` selector to group related selectors (pseudo-classes, pseudo-elements, child selectors, attribute selectors) under their parent instead of repeating the parent selector +- prefer container queries over media queries ## SQL @@ -76,6 +77,7 @@ - some a11y labels or text that should not normally be encountered by user (example given, error message by server) can be english - before adding a new translation, check that one doesn't already exist you can reuse (particularly in the common.json) - add only English translation and use `pnpm run i18n:sync` to initialize other jsons with empty string ready for translators +- when using namespace e.g. `const { t } = useTranslation("settings"]);` it needs to be defined in the `handle` for that route e.g. `export const handle: SendouRouteHandle = { i18n: ["settings"], ... }` ## Commit messages diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index e5e8b58e9..7b755c0a6 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; import type { Tables } from "~/db/tables"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; diff --git a/app/components/elements/Tabs.module.css b/app/components/elements/Tabs.module.css index c376693f5..152450cfd 100644 --- a/app/components/elements/Tabs.module.css +++ b/app/components/elements/Tabs.module.css @@ -94,3 +94,54 @@ z-index: 1; background-color: var(--color-bg); } + +.vertical { + display: grid; + grid-template-columns: max-content 1fr; + gap: var(--s-8); + align-items: start; + + & .tabListContainer { + overflow: visible; + position: sticky; + top: var(--layout-sticky-top); + align-self: start; + background-color: var(--color-bg); + z-index: 1; + } + + & .tabList { + flex-direction: column; + border-bottom: none; + border-inline-end: 2px solid var(--color-border); + min-width: 0; + } + + & .tabContainer { + margin-bottom: 0; + margin-inline-end: -2px; + + &[data-selected] .tabButton { + border-bottom-color: transparent; + border-inline-end-color: var(--color-text-accent); + } + } + + & .tabButton { + justify-content: flex-start; + border-bottom: none; + border-inline-end: 2px solid transparent; + text-align: start; + flex: none; + padding: var(--s-2) var(--s-3); + padding-inline-end: var(--s-6); + } + + & .tabPanel { + min-width: 0; + } + + &.padded .tabPanel { + padding-block-start: 0; + } +} diff --git a/app/components/elements/Tabs.tsx b/app/components/elements/Tabs.tsx index 38f0f3349..4fd63ccf6 100644 --- a/app/components/elements/Tabs.tsx +++ b/app/components/elements/Tabs.tsx @@ -9,6 +9,7 @@ import { Tabs, type TabsProps, } from "react-aria-components"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import buttonStyles from "./Button.module.css"; import styles from "./Tabs.module.css"; @@ -18,6 +19,8 @@ interface SendouTabsProps extends TabsProps { padded?: boolean; /** Hide tabs if only one tab shown? Defaults to true. */ disappearing?: boolean; + /** When orientation is "vertical", switch to horizontal once the main content width drops below this many pixels. */ + horizontalBelow?: number; } /** @@ -51,14 +54,31 @@ interface SendouTabsProps extends TabsProps { export function SendouTabs({ padded = true, disappearing = true, + horizontalBelow, className, + orientation, + onSelectionChange, ...rest }: SendouTabsProps) { + const mainWidth = useMainContentWidth(); + const collapsedToHorizontal = + orientation === "vertical" && + typeof horizontalBelow === "number" && + mainWidth > 0 && + mainWidth < horizontalBelow; + const effectiveOrientation = collapsedToHorizontal + ? "horizontal" + : orientation; + const isVertical = effectiveOrientation === "vertical"; + return ( diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 1b805ef42..e1bb063ee 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -13,6 +13,9 @@ import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import * as LFGRepository from "~/features/lfg/LFGRepository.server"; import { TIMEZONES } from "~/features/lfg/lfg-constants"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; +import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/match-profile/match-profile-constants"; import * as NotificationRepository from "~/features/notifications/NotificationRepository.server"; import type { Notification } from "~/features/notifications/notifications-types"; import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server"; @@ -26,9 +29,6 @@ import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.serv import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server"; import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server"; import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server"; -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; -import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/sendouq-settings/q-settings-constants"; import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server"; import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server"; import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server"; @@ -74,9 +74,9 @@ import { } from "../../../scripts/seed-art-urls"; import type { ParsedMemento, - QWeaponPool, Tables, UserMapModePreferences, + WeaponPoolEntry, } from "../tables"; import { ADMIN_TEST_AVATAR, @@ -183,7 +183,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [ adminUserWidgets, userProfiles, variation === "TEAM_MAP_PREFS" ? undefined : userMapModePreferences, - userQWeaponPool, + userMatchProfileWeaponPool, seedingSkills, lastMonthsVoting, syncPlusTiers, @@ -892,7 +892,7 @@ async function userProfiles() { if (faker.number.float(1) > 0.9) defaultLanguages.push("it"); if (faker.number.float(1) > 0.9) defaultLanguages.push("ja"); - await QSettingsRepository.updateVoiceChat({ + await MatchProfileRepository.updateVoiceChat({ languages: defaultLanguages, userId: id, vc: @@ -946,7 +946,7 @@ async function userMapModePreferences() { } } -async function userQWeaponPool() { +async function userMatchProfileWeaponPool() { for (let id = 1; id < 500; id++) { if (id === 2) continue; // no weapons for N-ZAP if (faker.number.float(1) < 0.2) continue; // 80% have weapons @@ -955,14 +955,14 @@ async function userQWeaponPool() { .shuffle(mainWeaponIds) .slice(0, faker.helpers.arrayElement([1, 2, 3, 4])); - const weaponPool: Array = weapons.map((weaponSplId) => ({ + const weaponPool: Array = weapons.map((weaponSplId) => ({ weaponSplId, isFavorite: faker.number.float(1) > 0.7 ? 1 : 0, })); await db .updateTable("User") - .set({ qWeaponPool: JSON.stringify(weaponPool) }) + .set({ weaponPool: JSON.stringify(weaponPool) }) .where("User.id", "=", id) .execute(); } @@ -2695,14 +2695,14 @@ async function groups(variation?: SeedVariation | null) { nzapGroupMemberIds[2], ].filter((id): id is number => typeof id === "number"); for (const userId of guaranteedWeaponPoolUserIds) { - const weapons: QWeaponPool[] = [ + const weapons: WeaponPoolEntry[] = [ { weaponSplId: 0, isFavorite: 1 }, { weaponSplId: 2000, isFavorite: 0 }, { weaponSplId: 4000, isFavorite: 0 }, ]; await db .updateTable("User") - .set({ qWeaponPool: JSON.stringify(weapons) }) + .set({ weaponPool: JSON.stringify(weapons) }) .where("User.id", "=", userId) .execute(); } diff --git a/app/db/tables.ts b/app/db/tables.ts index 6f8e45bc1..142a0970b 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -978,7 +978,7 @@ export interface UserMapModePreferences { }>; } -export interface QWeaponPool { +export interface WeaponPoolEntry { weaponSplId: MainWeaponId; isFavorite: number; } @@ -1074,7 +1074,7 @@ export interface User { vc: Generated<"YES" | "NO" | "LISTEN_ONLY">; youtubeId: string | null; mapModePreferences: JSONColumnTypeNullable; - qWeaponPool: JSONColumnTypeNullable; + weaponPool: JSONColumnTypeNullable; plusSkippedForSeasonNth: number | null; noScreen: Generated; /** User doesn't have access to SplatNet 3 to join rooms made by others */ diff --git a/app/features/front-page/loaders/index.server.ts b/app/features/front-page/loaders/index.server.ts index 6e4de50b4..6ce393611 100644 --- a/app/features/front-page/loaders/index.server.ts +++ b/app/features/front-page/loaders/index.server.ts @@ -4,8 +4,8 @@ import { getUser } from "~/features/auth/core/user.server"; import * as Changelog from "~/features/front-page/core/Changelog.server"; import { cachedFullUserLeaderboard } from "~/features/leaderboards/core/leaderboards.server"; import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; import * as Seasons from "~/features/mmr/core/Seasons"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server"; import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import { databaseTimestampNow } from "~/utils/dates"; @@ -33,8 +33,8 @@ export const loader = async () => { cachedLeaderboards(), SplatoonRotationRepository.findAll(), user - ? QSettingsRepository.settingsByUserId(user.id).then( - (s) => s.qWeaponPool ?? null, + ? MatchProfileRepository.settingsByUserId(user.id).then( + (s) => s.weaponPool ?? null, ) : Promise.resolve(null), ]); diff --git a/app/features/lfg/lfg-schemas.ts b/app/features/lfg/lfg-schemas.ts index 3b80623bb..00e68beef 100644 --- a/app/features/lfg/lfg-schemas.ts +++ b/app/features/lfg/lfg-schemas.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { LANGUAGE_OPTIONS } from "~/features/sendouq-settings/q-settings-schemas"; +import { LANGUAGE_OPTIONS } from "~/features/settings/match-profile-schemas"; import { checkboxGroup, idConstantOptional, diff --git a/app/features/lfg/loaders/lfg.new.server.ts b/app/features/lfg/loaders/lfg.new.server.ts index 56cdc8395..d359a9a4d 100644 --- a/app/features/lfg/loaders/lfg.new.server.ts +++ b/app/features/lfg/loaders/lfg.new.server.ts @@ -1,7 +1,7 @@ import type { LoaderFunctionArgs } from "react-router"; import { z } from "zod"; import { requireUser } from "~/features/auth/core/user.server"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { parseSafeSearchParams } from "~/utils/remix.server"; import type { Unpacked } from "~/utils/types"; @@ -14,14 +14,16 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const userProfileData = await UserRepository.findProfileByIdentifier( String(user.id), ); - const userQSettingsData = await QSettingsRepository.settingsByUserId(user.id); + const userMatchProfile = await MatchProfileRepository.settingsByUserId( + user.id, + ); const allPosts = await LFGRepository.posts(user); const postToEdit = searchParamsToBuildToEdit(request, user.id, allPosts); return { team: userProfileData?.team, weaponPool: userProfileData?.weapons, - languages: postToEdit?.languages?.split(",") ?? userQSettingsData.languages, + languages: postToEdit?.languages?.split(",") ?? userMatchProfile.languages, postToEdit, userPostTypes: userPostTypes(allPosts, user.id), }; diff --git a/app/features/sendouq-settings/QSettingsRepository.server.ts b/app/features/match-profile/MatchProfileRepository.server.ts similarity index 65% rename from app/features/sendouq-settings/QSettingsRepository.server.ts rename to app/features/match-profile/MatchProfileRepository.server.ts index 8f749feb1..6b9174da5 100644 --- a/app/features/sendouq-settings/QSettingsRepository.server.ts +++ b/app/features/match-profile/MatchProfileRepository.server.ts @@ -11,7 +11,7 @@ export async function settingsByUserId(userId: number) { "User.mapModePreferences", "User.vc", "User.languages", - "User.qWeaponPool", + "User.weaponPool", "User.noScreen", "User.noSplatnet", ]) @@ -26,12 +26,37 @@ export async function settingsByUserId(userId: number) { }; } -export async function updateUserMapModePreferences({ +export function updateVoiceChat(args: { + userId: number; + vc: Tables["User"]["vc"]; + languages: string[]; +}) { + return db + .updateTable("User") + .set({ + vc: args.vc, + languages: args.languages.length > 0 ? args.languages.join(",") : null, + }) + .where("User.id", "=", args.userId) + .execute(); +} + +export async function updateMatchProfile({ userId, mapModePreferences, + vc, + languages, + weaponPool, + noScreen, + noSplatnet, }: { userId: number; mapModePreferences: UserMapModePreferences; + vc: Tables["User"]["vc"]; + languages: string[]; + weaponPool: WeaponPoolItem[]; + noScreen: number; + noSplatnet: number; }) { const currentPreferences = ( await db @@ -53,108 +78,21 @@ export async function updateUserMapModePreferences({ ...mapModePreferences, pool: mergedPool, }), - }) - .where("id", "=", userId) - .execute(); -} - -export async function updateTeamMapModePreferences({ - teamId, - mapModePreferences, -}: { - teamId: number; - mapModePreferences: UserMapModePreferences; -}) { - const currentPreferences = ( - await db - .selectFrom("AllTeam") - .select("mapModePreferences") - .where("id", "=", teamId) - .executeTakeFirstOrThrow() - ).mapModePreferences; - - const mergedPool = mergeExcludedModePreferences( - mapModePreferences.pool, - currentPreferences?.pool, - ); - - return db - .updateTable("AllTeam") - .set({ - mapModePreferences: JSON.stringify({ - ...mapModePreferences, - pool: mergedPool, - }), - }) - .where("id", "=", teamId) - .execute(); -} - -export function updateVoiceChat(args: { - userId: number; - vc: Tables["User"]["vc"]; - languages: string[]; -}) { - return db - .updateTable("User") - .set({ - vc: args.vc, - languages: args.languages.length > 0 ? args.languages.join(",") : null, - }) - .where("User.id", "=", args.userId) - .execute(); -} - -export function updateSendouQWeaponPool(args: { - userId: number; - weaponPool: WeaponPoolItem[]; -}) { - return db - .updateTable("User") - .set({ - qWeaponPool: - args.weaponPool.length > 0 + vc, + languages: languages.length > 0 ? languages.join(",") : null, + weaponPool: + weaponPool.length > 0 ? JSON.stringify( - args.weaponPool.map((wpn) => ({ + weaponPool.map((wpn) => ({ weaponSplId: wpn.id, isFavorite: Number(wpn.isFavorite), })), ) : null, - }) - .where("User.id", "=", args.userId) - .execute(); -} - -export function updateNoScreen({ - noScreen, - userId, -}: { - noScreen: number; - userId: number; -}) { - return db - .updateTable("User") - .set({ noScreen, - }) - .where("User.id", "=", userId) - .execute(); -} - -export function updateNoSplatnet({ - noSplatnet, - userId, -}: { - noSplatnet: number; - userId: number; -}) { - return db - .updateTable("User") - .set({ noSplatnet, }) - .where("User.id", "=", userId) + .where("id", "=", userId) .execute(); } diff --git a/app/features/sendouq-settings/banned-maps.ts b/app/features/match-profile/banned-maps.ts similarity index 100% rename from app/features/sendouq-settings/banned-maps.ts rename to app/features/match-profile/banned-maps.ts diff --git a/app/features/match-profile/match-profile-constants.ts b/app/features/match-profile/match-profile-constants.ts new file mode 100644 index 000000000..19579b974 --- /dev/null +++ b/app/features/match-profile/match-profile-constants.ts @@ -0,0 +1,3 @@ +export const MATCH_PROFILE_WEAPON_POOL_MAX_SIZE = 4; + +export const AMOUNT_OF_MAPS_IN_POOL_PER_MODE = 7; diff --git a/app/features/match-profile/routes/q.settings.tsx b/app/features/match-profile/routes/q.settings.tsx new file mode 100644 index 000000000..ba71d925f --- /dev/null +++ b/app/features/match-profile/routes/q.settings.tsx @@ -0,0 +1,10 @@ +import { redirect } from "react-router"; +import { MATCH_PROFILE_PAGE } from "~/utils/urls"; + +export const loader = () => { + throw redirect(MATCH_PROFILE_PAGE); +}; + +export default function MatchProfileRedirect() { + return null; +} diff --git a/app/features/sendouq-match/SQMatchRepository.server.ts b/app/features/sendouq-match/SQMatchRepository.server.ts index 8df3c155c..919f9bb76 100644 --- a/app/features/sendouq-match/SQMatchRepository.server.ts +++ b/app/features/sendouq-match/SQMatchRepository.server.ts @@ -144,7 +144,7 @@ function groupWithTeamAndMembers( "User.vc", "User.languages", "User.noScreen", - "User.qWeaponPool as weapons", + "User.weaponPool as weapons", "User.mapModePreferences", "PlusTier.tier as plusTier", "GroupMatchContinueVote.isContinuing", diff --git a/app/features/sendouq-match/core/match.server.ts b/app/features/sendouq-match/core/match.server.ts index 640e219bd..e2e167e66 100644 --- a/app/features/sendouq-match/core/match.server.ts +++ b/app/features/sendouq-match/core/match.server.ts @@ -2,6 +2,10 @@ import * as R from "remeda"; import type { ParsedMemento, UserMapModePreferences } from "~/db/tables"; import * as MapList from "~/features/map-list-generator/core/MapList"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; +import { + BANNED_MAPS, + SENDOUQ_MAP_POOL, +} from "~/features/match-profile/banned-maps"; import * as Seasons from "~/features/mmr/core/Seasons"; import { userSkills } from "~/features/mmr/tiered.server"; import { getDefaultMapWeights } from "~/features/sendouq/core/default-maps.server"; @@ -10,10 +14,6 @@ import type { SQUncensoredGroup, } from "~/features/sendouq/core/SendouQ.server"; import { SENDOUQ_BEST_OF } from "~/features/sendouq/q-constants"; -import { - BANNED_MAPS, - SENDOUQ_MAP_POOL, -} from "~/features/sendouq-settings/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; import type { ModeShort, ModeWithStage } from "~/modules/in-game-lists/types"; import type { diff --git a/app/features/sendouq-settings/actions/q.settings.server.ts b/app/features/sendouq-settings/actions/q.settings.server.ts deleted file mode 100644 index 72cfcf3fe..000000000 --- a/app/features/sendouq-settings/actions/q.settings.server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { requireUser } from "~/features/auth/core/user.server"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; -import * as TeamRepository from "~/features/team/TeamRepository.server"; -import { parseRequestPayload } from "~/utils/remix.server"; -import { assertUnreachable } from "~/utils/types"; -import { settingsActionSchema } from "../q-settings-schemas.server"; - -export const action = async ({ request }: { request: Request }) => { - const user = requireUser(); - const data = await parseRequestPayload({ - request, - schema: settingsActionSchema, - }); - - switch (data._action) { - case "UPDATE_MAP_MODE_PREFERENCES": { - if (typeof data.teamId === "number") { - const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id); - const canManage = allTeams.some( - (t) => t.id === data.teamId && (t.isOwner || t.isManager), - ); - if (!canManage) { - throw new Response(null, { status: 403 }); - } - - await QSettingsRepository.updateTeamMapModePreferences({ - mapModePreferences: data.mapModePreferences, - teamId: data.teamId, - }); - } else { - await QSettingsRepository.updateUserMapModePreferences({ - mapModePreferences: data.mapModePreferences, - userId: user.id, - }); - } - break; - } - case "UPDATE_VC": { - await QSettingsRepository.updateVoiceChat({ - userId: user.id, - vc: data.vc, - languages: data.languages, - }); - break; - } - case "UPDATE_SENDOUQ_WEAPON_POOL": { - await QSettingsRepository.updateSendouQWeaponPool({ - userId: user.id, - weaponPool: data.weaponPool, - }); - break; - } - default: { - assertUnreachable(data); - } - } - - return { ok: true }; -}; diff --git a/app/features/sendouq-settings/loaders/q.settings.server.ts b/app/features/sendouq-settings/loaders/q.settings.server.ts deleted file mode 100644 index 57e9806d6..000000000 --- a/app/features/sendouq-settings/loaders/q.settings.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { requireUser } from "~/features/auth/core/user.server"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; -import * as TeamRepository from "~/features/team/TeamRepository.server"; - -export const loader = async () => { - const user = requireUser(); - - const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id); - const manageableTeams = allTeams.filter((t) => t.isOwner || t.isManager); - - return { - settings: await QSettingsRepository.settingsByUserId(user.id), - manageableTeams, - }; -}; diff --git a/app/features/sendouq-settings/q-settings-constants.ts b/app/features/sendouq-settings/q-settings-constants.ts deleted file mode 100644 index 434ea6079..000000000 --- a/app/features/sendouq-settings/q-settings-constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const SENDOUQ_WEAPON_POOL_MAX_SIZE = 4; - -export const AMOUNT_OF_MAPS_IN_POOL_PER_MODE = 7; diff --git a/app/features/sendouq-settings/q-settings-schemas.server.ts b/app/features/sendouq-settings/q-settings-schemas.server.ts deleted file mode 100644 index 24528aace..000000000 --- a/app/features/sendouq-settings/q-settings-schemas.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from "zod"; -import { _action, id, modeShort, safeJSONParse, stageId } from "~/utils/zod"; -import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "./q-settings-constants"; -import { - updateVoiceChatSchema, - updateWeaponPoolSchema, -} from "./q-settings-schemas"; - -const preference = z.enum(["AVOID", "PREFER"]).optional(); -export const settingsActionSchema = z.union([ - z.object({ - _action: _action("UPDATE_MAP_MODE_PREFERENCES"), - teamId: z.preprocess( - (val) => (val === "" || val === undefined ? undefined : Number(val)), - id.optional(), - ), - mapModePreferences: z.preprocess( - safeJSONParse, - z - .object({ - modes: z.array(z.object({ mode: modeShort, preference })), - pool: z.array( - z.object({ - stages: z.array(stageId).max(AMOUNT_OF_MAPS_IN_POOL_PER_MODE), - mode: modeShort, - }), - ), - }) - .refine( - (val) => - val.pool.every((pool) => { - const mp = val.modes.find((m) => m.mode === pool.mode); - return mp?.preference !== "AVOID"; - }), - "Can't have map pool for a mode that was avoided", - ), - ), - }), - updateVoiceChatSchema, - updateWeaponPoolSchema, -]); diff --git a/app/features/sendouq-settings/q-settings-schemas.ts b/app/features/sendouq-settings/q-settings-schemas.ts deleted file mode 100644 index 6ef5a0e99..000000000 --- a/app/features/sendouq-settings/q-settings-schemas.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from "zod"; -import { - checkboxGroup, - radioGroup, - stringConstant, - weaponPool, -} from "~/form/fields"; -import { languagesUnified } from "~/modules/i18n/config"; -import { SENDOUQ_WEAPON_POOL_MAX_SIZE } from "./q-settings-constants"; - -export const updateWeaponPoolSchema = z.object({ - _action: stringConstant("UPDATE_SENDOUQ_WEAPON_POOL"), - weaponPool: weaponPool({ - label: "labels.weaponPool", - maxCount: SENDOUQ_WEAPON_POOL_MAX_SIZE, - }), -}); - -export const LANGUAGE_OPTIONS = languagesUnified.map((lang) => ({ - label: () => lang.name, - value: lang.code, -})); - -export const updateVoiceChatSchema = z.object({ - _action: stringConstant("UPDATE_VC"), - vc: radioGroup({ - label: "labels.voiceChat", - items: [ - { label: "options.voiceChat.yes", value: "YES" }, - { label: "options.voiceChat.no", value: "NO" }, - { label: "options.voiceChat.listenOnly", value: "LISTEN_ONLY" }, - ], - }), - languages: checkboxGroup({ - label: "labels.languages", - items: LANGUAGE_OPTIONS, - }), -}); diff --git a/app/features/sendouq-settings/routes/q.settings.module.css b/app/features/sendouq-settings/routes/q.settings.module.css deleted file mode 100644 index 61c81b1b8..000000000 --- a/app/features/sendouq-settings/routes/q.settings.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.summary { - padding: var(--s-3); - border-radius: var(--radius-box); - background-color: var(--color-bg-high); - font-size: var(--font-lg); - font-weight: var(--weight-bold); - margin-block-end: var(--s-4); - position: relative; - - & > div { - display: inline-flex; - } - - & svg { - width: 24px; - color: var(--color-text-accent); - position: absolute; - right: 20px; - top: 14px; - } -} - -.volumeSliderIcon { - width: 16px; - height: 16px; -} - -/* -Necessary because default style adds padding, making the slider not go from 0 to 1 visually -Changing the default style would affect all other input elements, so we need to override it -*/ -.volumeSliderInput { - padding-left: 0 !important; - padding-right: 0 !important; - border: 0 !important; -} diff --git a/app/features/sendouq-settings/routes/q.settings.tsx b/app/features/sendouq-settings/routes/q.settings.tsx deleted file mode 100644 index 3bf1e47e0..000000000 --- a/app/features/sendouq-settings/routes/q.settings.tsx +++ /dev/null @@ -1,538 +0,0 @@ -import clsx from "clsx"; -import { Map as MapIcon, Mic, Puzzle, Volume2 } from "lucide-react"; -import * as React from "react"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import type { MetaFunction } from "react-router"; -import { useFetcher, useLoaderData } from "react-router"; -import { Avatar } from "~/components/Avatar"; -import { SendouSelect, SendouSelectItem } from "~/components/elements/Select"; -import { ModeImage } from "~/components/Image"; -import { Main } from "~/components/Main"; -import { SubmitButton } from "~/components/SubmitButton"; -import type { Preference, UserMapModePreferences } from "~/db/tables"; -import { useUser } from "~/features/auth/core/user"; -import { - soundCodeToLocalStorageKey, - soundVolume, -} from "~/features/chat/chat-utils"; -import { updateNoScreenSchema } from "~/features/settings/settings-schemas"; -import { SendouForm } from "~/form/SendouForm"; -import { useHydrated } from "~/hooks/useHydrated"; -import { modesShort } from "~/modules/in-game-lists/modes"; -import type { ModeShort } from "~/modules/in-game-lists/types"; -import type { SerializeFrom } from "~/utils/remix"; -import { metaTags } from "~/utils/remix"; -import type { SendouRouteHandle } from "~/utils/remix.server"; -import { - navIconUrl, - SENDOUQ_PAGE, - SENDOUQ_SETTINGS_PAGE, - SETTINGS_PAGE, - soundPath, -} from "~/utils/urls"; -import { action } from "../actions/q.settings.server"; -import { BANNED_MAPS } from "../banned-maps"; -import { ModeMapPoolPicker } from "../components/ModeMapPoolPicker"; -import { PreferenceRadioGroup } from "../components/PreferenceRadioGroup"; -import { loader } from "../loaders/q.settings.server"; -import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "../q-settings-constants"; -import { - updateVoiceChatSchema, - updateWeaponPoolSchema, -} from "../q-settings-schemas"; - -export { action, loader }; - -import styles from "./q.settings.module.css"; - -export const handle: SendouRouteHandle = { - i18n: ["q"], - breadcrumb: () => [ - { - imgPath: navIconUrl("sendouq"), - href: SENDOUQ_PAGE, - type: "IMAGE", - }, - { - imgPath: navIconUrl("settings"), - href: SENDOUQ_SETTINGS_PAGE, - type: "IMAGE", - }, - ], -}; - -export const meta: MetaFunction = (args) => { - return metaTags({ - title: "SendouQ - Settings", - location: args.location, - }); -}; - -export default function SendouQSettingsPage() { - return ( -
- -
- - - - -
-
- ); -} - -const PERSONAL_KEY = "personal"; - -type ManageableTeam = SerializeFrom["manageableTeams"][number]; - -function preferencesFromRaw( - raw: UserMapModePreferences | null, -): UserMapModePreferences { - if (!raw) return { pool: [], modes: [] }; - - return { - modes: raw.modes, - pool: raw.pool.map((p) => ({ - mode: p.mode, - stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)), - })), - }; -} - -function MapPicker() { - const { t } = useTranslation(["q", "common"]); - const data = useLoaderData(); - const fetcher = useFetcher(); - const hasTeams = data.manageableTeams.length > 0; - - const [selectedKey, setSelectedKey] = useState(PERSONAL_KEY); - - const selectedTeamId = - selectedKey === PERSONAL_KEY ? undefined : Number(selectedKey); - - const preferencesForSelection = (key: string) => { - if (key === PERSONAL_KEY) { - return preferencesFromRaw(data.settings.mapModePreferences); - } - const team = data.manageableTeams.find((t) => String(t.id) === key); - return preferencesFromRaw(team?.mapModePreferences ?? null); - }; - - const [preferences, setPreferences] = useState(() => - preferencesForSelection(PERSONAL_KEY), - ); - - const handleSelectionChange = (key: string) => { - setSelectedKey(key); - setPreferences(preferencesForSelection(key)); - }; - - const handleModePreferenceChange = ({ - mode, - preference, - }: { - mode: ModeShort; - preference: Preference & "NEUTRAL"; - }) => { - const newModePreferences = preferences.modes.filter( - (map) => map.mode !== mode, - ); - - if (preference !== "NEUTRAL") { - newModePreferences.push({ - mode, - preference, - }); - } - - setPreferences({ - ...preferences, - modes: newModePreferences, - }); - }; - - const poolsOk = () => { - for (const mode of modesShort) { - const mp = preferences.modes.find( - (preference) => preference.mode === mode, - ); - if (mp?.preference === "AVOID") continue; - - const pool = preferences.pool.find((p) => p.mode === mode); - if (pool && pool.stages.length > AMOUNT_OF_MAPS_IN_POOL_PER_MODE) { - return false; - } - } - - return true; - }; - - const selectItems = [ - { id: PERSONAL_KEY, name: t("q:settings.maps.personal") }, - ...data.manageableTeams.map((team) => ({ - id: String(team.id), - name: team.name, - })), - ]; - - return ( -
- -
- {t("q:settings.maps.header")} -
-
- - { - const isAvoided = - preferences.modes.find((m) => m.mode === p.mode)?.preference === - "AVOID"; - - return !isAvoided; - }), - })} - /> - {selectedTeamId ? ( - - ) : null} -
- {hasTeams ? ( -
- handleSelectionChange(String(key))} - aria-label={t("q:settings.maps.preferencesFor")} - items={selectItems} - bottomText={t("q:settings.maps.teamExplanation")} - > - {(item) => ( - - - - )} - -
- ) : null} -
- {modesShort.map((modeShort) => { - const preference = preferences.modes.find( - (preference) => preference.mode === modeShort, - ); - - return ( -
- - - handleModePreferenceChange({ - mode: modeShort, - preference, - }) - } - aria-label={`Select preference towards ${modeShort}`} - /> -
- ); - })} -
- -
- {modesShort.map((mode) => { - const mp = preferences.modes.find( - (preference) => preference.mode === mode, - ); - if (mp?.preference === "AVOID") return null; - - return ( - p.mode === mode)?.stages ?? [] - } - onChange={(stages) => { - const newPools = preferences.pool.filter( - (p) => p.mode !== mode, - ); - newPools.push({ mode, stages }); - setPreferences({ - ...preferences, - pool: newPools, - }); - }} - /> - ); - })} -
-
-
- {poolsOk() ? ( - - {t("common:actions.save")} - - ) : ( -
- {t("q:settings.mapPool.notOk", { - count: AMOUNT_OF_MAPS_IN_POOL_PER_MODE, - })} -
- )} -
-
-
- ); -} - -function MapPickerSelectOption({ - item, - teams, -}: { - item: { id: string; name: string }; - teams: ManageableTeam[]; -}) { - const user = useUser(); - - if (item.id === PERSONAL_KEY) { - return ( -
- - {item.name} -
- ); - } - - const team = teams.find((t) => String(t.id) === item.id); - if (!team) return item.name; - - return ( -
- - {team.name} -
- ); -} - -function VoiceChat() { - const { t } = useTranslation(["q"]); - const data = useLoaderData(); - - return ( -
- -
- {t("q:settings.voiceChat.header")} -
-
-
- - {({ FormField }) => ( - <> - - - - )} - -
-
- ); -} - -function WeaponPool() { - const { t } = useTranslation(["q"]); - const data = useLoaderData(); - - const defaultWeaponPool = (data.settings.qWeaponPool ?? []).map((w) => ({ - id: w.weaponSplId, - isFavorite: Boolean(w.isFavorite), - })); - - return ( -
- -
- {t("q:settings.weaponPool.header")} -
-
-
- - {({ FormField }) => } - -
-
- ); -} - -function Sounds() { - const { t } = useTranslation(["q"]); - const isHydrated = useHydrated(); - - return ( -
- -
- {t("q:settings.sounds.header")} -
-
-
- {isHydrated && } - {isHydrated && } -
-
- ); -} - -function SoundCheckboxes() { - const { t } = useTranslation(["q"]); - - const sounds = [ - { - code: "sq_like", - name: t("q:settings.sounds.likeReceived"), - }, - { - code: "sq_new-group", - name: t("q:settings.sounds.groupNewMember"), - }, - { - code: "sq_match", - name: t("q:settings.sounds.matchStarted"), - }, - { - code: "tournament_match", - name: t("q:settings.sounds.tournamentMatchStarted"), - }, - ]; - - // default to true - const currentValue = (code: string) => - !localStorage.getItem(soundCodeToLocalStorageKey(code)) || - localStorage.getItem(soundCodeToLocalStorageKey(code)) === "true"; - - const [soundValues, setSoundValues] = React.useState( - Object.fromEntries( - sounds.map((sound) => [sound.code, currentValue(sound.code)]), - ), - ); - - // toggle in local storage - const toggleSound = (code: string) => { - localStorage.setItem( - soundCodeToLocalStorageKey(code), - String(!currentValue(code)), - ); - setSoundValues((prev) => ({ - ...prev, - [code]: !prev[code], - })); - }; - - return ( -
- {sounds.map((sound) => ( -
- -
- ))} -
- ); -} - -function SoundSlider() { - const [volume, setVolume] = useState(() => { - return soundVolume() || 100; - }); - - const changeVolume = (event: React.ChangeEvent) => { - const newVolume = Number.parseFloat(event.target.value); - - setVolume(newVolume); - - localStorage.setItem( - "settings__sound-volume", - String(Math.floor(newVolume)), - ); - }; - - const playSound = () => { - const audio = new Audio(soundPath("sq_like")); - audio.volume = soundVolume() / 100; - void audio.play(); - }; - - return ( -
- - -
- ); -} - -function Misc() { - const data = useLoaderData(); - const { t } = useTranslation(["q"]); - - return ( -
- -
{t("q:settings.misc.header")}
-
-
- - {({ FormField }) => } - -
-
- ); -} diff --git a/app/features/sendouq/SQGroupRepository.server.ts b/app/features/sendouq/SQGroupRepository.server.ts index d5515dfa4..e74805da1 100644 --- a/app/features/sendouq/SQGroupRepository.server.ts +++ b/app/features/sendouq/SQGroupRepository.server.ts @@ -61,7 +61,7 @@ export async function findCurrentGroups() { vc: Tables["User"]["vc"]; role: Tables["GroupMember"]["role"]; note: Tables["GroupMember"]["note"]; - weapons: Tables["User"]["qWeaponPool"]; + weapons: Tables["User"]["weaponPool"]; plusTier: Tables["PlusTier"]["tier"] | null; }; @@ -99,7 +99,7 @@ export async function findCurrentGroups() { noScreen: eb.ref("User.noScreen"), role: eb.ref("GroupMember.role"), note: eb.ref("GroupMember.note"), - weapons: eb.ref("User.qWeaponPool"), + weapons: eb.ref("User.weaponPool"), languages: eb.ref("User.languages"), plusTier: eb.ref("PlusTier.tier"), vc: eb.ref("User.vc"), diff --git a/app/features/sendouq/routes/q.info.tsx b/app/features/sendouq/routes/q.info.tsx index 0d5861755..613b50d47 100644 --- a/app/features/sendouq/routes/q.info.tsx +++ b/app/features/sendouq/routes/q.info.tsx @@ -9,9 +9,9 @@ import { metaTags } from "~/utils/remix"; import { CALENDAR_PAGE, FAQ_PAGE, + MATCH_PROFILE_PAGE, navIconUrl, SENDOUQ_RULES_PAGE, - SENDOUQ_SETTINGS_PAGE, TIERS_PAGE, } from "~/utils/urls"; import styles from "./q.info.module.css"; @@ -180,7 +180,7 @@ function BeforeJoining() {

Optional - if you don't have a preference then skip this step and other players in the lobby get to choose. On the{" "} - settings page first select your + settings page first select your preference of each of the five modes. Avoid means you'd rather not play the mode. Neutral means you don't have strong feelings either way. Prefer means you like this mode over neutral modes. These choices diff --git a/app/features/sendouq/routes/q.looking.test.ts b/app/features/sendouq/routes/q.looking.test.ts index 1bcf93c40..122a26fc1 100644 --- a/app/features/sendouq/routes/q.looking.test.ts +++ b/app/features/sendouq/routes/q.looking.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { db } from "~/db/sql"; import type { UserMapModePreferences } from "~/db/tables"; -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import invariant from "~/utils/invariant"; import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test"; diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index 131a79d05..72b208898 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -24,10 +24,10 @@ import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { + MATCH_PROFILE_PAGE, navIconUrl, SENDOUQ_LOOKING_PAGE, SENDOUQ_PAGE, - SENDOUQ_SETTINGS_PAGE, SENDOUQ_STREAMS_PAGE, } from "~/utils/urls"; import { action } from "../actions/q.looking.server"; @@ -173,7 +173,7 @@ function InfoText() { >

diff --git a/app/features/settings/actions/settings.server.ts b/app/features/settings/actions/settings.server.ts index 157d7a783..77b761479 100644 --- a/app/features/settings/actions/settings.server.ts +++ b/app/features/settings/actions/settings.server.ts @@ -1,18 +1,18 @@ import type { ActionFunctionArgs } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; -import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { isSupporter } from "~/modules/permissions/utils"; import { clampThemeToGamut } from "~/utils/oklch-gamut"; import { errorToast, parseRequestPayload } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; -import { settingsEditSchema } from "../settings-schemas"; +import { settingsActionSchema } from "../settings-schemas.server"; export const action = async ({ request }: ActionFunctionArgs) => { const user = requireUser(); const data = await parseRequestPayload({ request, - schema: settingsEditSchema, + schema: settingsActionSchema, }); switch (data._action) { @@ -46,20 +46,6 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); break; } - case "UPDATE_NO_SCREEN": { - await QSettingsRepository.updateNoScreen({ - userId: user.id, - noScreen: Number(data.newValue), - }); - break; - } - case "UPDATE_NO_SPLATNET": { - await QSettingsRepository.updateNoSplatnet({ - userId: user.id, - noSplatnet: Number(data.newValue), - }); - break; - } case "UPDATE_CLOCK_FORMAT": { await UserRepository.updatePreferences(user.id, { clockFormat: data.newValue, @@ -72,12 +58,22 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); break; } + case "UPDATE_MATCH_PROFILE": { + await MatchProfileRepository.updateMatchProfile({ + userId: user.id, + mapModePreferences: data.mapModePreferences, + vc: data.vc, + languages: data.languages, + weaponPool: data.weaponPool, + noScreen: Number(data.noScreen), + noSplatnet: Number(data.noSplatnet), + }); + break; + } default: { assertUnreachable(data); } } - // TODO: removed temporarily, restore when we have better toasts - // (current problem is that when you update no screen from /q/settings, you get redirected to /settings) - // return successToast("Settings updated"); + return null; }; diff --git a/app/features/settings/components/LocaleTab.tsx b/app/features/settings/components/LocaleTab.tsx new file mode 100644 index 000000000..482f056e4 --- /dev/null +++ b/app/features/settings/components/LocaleTab.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from "react-i18next"; +import { useNavigate, useSearchParams } from "react-router"; +import { useUser } from "~/features/auth/core/user"; +import { SelectFormField } from "~/form/fields/SelectFormField"; +import { SendouForm } from "~/form/SendouForm"; +import { languages } from "~/modules/i18n/config"; +import { clockFormatSchema } from "../settings-schemas"; + +export function LocaleTab() { + const user = useUser(); + + return ( +
+ + {user ? ( + + {({ FormField }) => } + + ) : null} +
+ ); +} + +function LanguageSelector() { + const { t, i18n } = useTranslation(["common"]); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const languageItems = languages.map((lang) => ({ + value: lang.code, + label: lang.name, + })); + + const handleLanguageChange = (newLang: string | null) => { + if (!newLang) return; + const next = new URLSearchParams(searchParams); + next.delete("lng"); + next.append("lng", newLang); + navigate(`?${next.toString()}`); + }; + + return ( + + ); +} diff --git a/app/features/settings/components/MatchProfileTab.tsx b/app/features/settings/components/MatchProfileTab.tsx new file mode 100644 index 000000000..b8ae2d1fe --- /dev/null +++ b/app/features/settings/components/MatchProfileTab.tsx @@ -0,0 +1,146 @@ +import { useLoaderData } from "react-router"; +import { ModeImage } from "~/components/Image"; +import type { Preference, UserMapModePreferences } from "~/db/tables"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; +import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/match-profile/match-profile-constants"; +import { SendouForm } from "~/form/SendouForm"; +import { modesShort } from "~/modules/in-game-lists/modes"; +import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; +import type { loader } from "../loaders/settings.server"; +import { updateMatchProfileSchema } from "../match-profile-schemas"; +import { ModeMapPoolPicker } from "./ModeMapPoolPicker"; +import { PreferenceRadioGroup } from "./PreferenceRadioGroup"; + +export function MatchProfileTab() { + const data = useLoaderData(); + const matchProfile = data.matchProfile; + + if (!matchProfile) return null; + + return ( + ({ + id: w.weaponSplId, + isFavorite: Boolean(w.isFavorite), + })), + vc: matchProfile.vc ?? "NO", + languages: matchProfile.languages ?? [], + noScreen: Boolean(matchProfile.noScreen), + noSplatnet: Boolean(matchProfile.noSplatnet), + }} + revalidateRoot + > + {({ FormField }) => ( + <> + + {(props: { + value: unknown; + onChange: (value: UserMapModePreferences) => void; + }) => ( + + )} + + + + + + + + )} + + ); +} + +function preferencesFromRaw( + raw: UserMapModePreferences | null, +): UserMapModePreferences { + if (!raw) return { pool: [], modes: [] }; + + return { + modes: raw.modes, + pool: raw.pool.map((p) => ({ + mode: p.mode, + stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)), + })), + }; +} + +function MapModePreferencesField({ + value, + onChange, +}: { + value: UserMapModePreferences; + onChange: (value: UserMapModePreferences) => void; +}) { + const handleModePreferenceChange = ({ + mode, + preference, + }: { + mode: ModeShort; + preference: Preference & "NEUTRAL"; + }) => { + const newModePreferences = value.modes.filter((map) => map.mode !== mode); + if (preference !== "NEUTRAL") { + newModePreferences.push({ mode, preference }); + } + const newPool = + preference === "AVOID" + ? value.pool.filter((p) => p.mode !== mode) + : value.pool; + onChange({ modes: newModePreferences, pool: newPool }); + }; + + const handlePoolChange = (mode: ModeShort, stages: StageId[]) => { + const filtered = value.pool.filter((p) => p.mode !== mode); + filtered.push({ mode, stages }); + onChange({ ...value, pool: filtered }); + }; + + return ( +
+
+ {modesShort.map((modeShort) => { + const preference = value.modes.find( + (preference) => preference.mode === modeShort, + ); + + return ( +
+ + + handleModePreferenceChange({ mode: modeShort, preference }) + } + aria-label={`Select preference towards ${modeShort}`} + /> +
+ ); + })} +
+ +
+ {modesShort.map((mode) => { + const mp = value.modes.find((p) => p.mode === mode); + if (mp?.preference === "AVOID") return null; + + return ( + p.mode === mode)?.stages ?? []} + onChange={(stages) => handlePoolChange(mode, stages)} + /> + ); + })} +
+
+ ); +} diff --git a/app/features/sendouq-settings/components/ModeMapPoolPicker.module.css b/app/features/settings/components/ModeMapPoolPicker.module.css similarity index 100% rename from app/features/sendouq-settings/components/ModeMapPoolPicker.module.css rename to app/features/settings/components/ModeMapPoolPicker.module.css diff --git a/app/features/sendouq-settings/components/ModeMapPoolPicker.tsx b/app/features/settings/components/ModeMapPoolPicker.tsx similarity index 98% rename from app/features/sendouq-settings/components/ModeMapPoolPicker.tsx rename to app/features/settings/components/ModeMapPoolPicker.tsx index 8cb9f3ce3..99ea5f85e 100644 --- a/app/features/sendouq-settings/components/ModeMapPoolPicker.tsx +++ b/app/features/settings/components/ModeMapPoolPicker.tsx @@ -4,11 +4,11 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { Divider } from "~/components/Divider"; import { ModeImage } from "~/components/Image"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { shortStageName, stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import { nullFilledArray } from "~/utils/arrays"; import { stageImageUrl } from "~/utils/urls"; -import { BANNED_MAPS } from "../banned-maps"; import styles from "./ModeMapPoolPicker.module.css"; export function ModeMapPoolPicker({ diff --git a/app/features/sendouq-settings/components/PreferenceRadioGroup.module.css b/app/features/settings/components/PreferenceRadioGroup.module.css similarity index 100% rename from app/features/sendouq-settings/components/PreferenceRadioGroup.module.css rename to app/features/settings/components/PreferenceRadioGroup.module.css diff --git a/app/features/sendouq-settings/components/PreferenceRadioGroup.tsx b/app/features/settings/components/PreferenceRadioGroup.tsx similarity index 91% rename from app/features/sendouq-settings/components/PreferenceRadioGroup.tsx rename to app/features/settings/components/PreferenceRadioGroup.tsx index 4f9ecd40a..833f87c29 100644 --- a/app/features/sendouq-settings/components/PreferenceRadioGroup.tsx +++ b/app/features/settings/components/PreferenceRadioGroup.tsx @@ -14,7 +14,7 @@ export function PreferenceRadioGroup({ onPreferenceChange: (preference: Preference & "NEUTRAL") => void; "aria-label": string; }) { - const { t } = useTranslation(["q"]); + const { t } = useTranslation(["settings"]); return ( - {t("q:settings.maps.avoid")} + {t("settings:matchProfile.maps.avoid")} )} @@ -58,7 +58,7 @@ export function PreferenceRadioGroup({ width={18} alt="Neutral emoji" /> - {t("q:settings.maps.neutral")} + {t("settings:matchProfile.maps.neutral")} )} @@ -76,7 +76,7 @@ export function PreferenceRadioGroup({ width={18} alt="Prefer emoji" /> - {t("q:settings.maps.prefer")} + {t("settings:matchProfile.maps.prefer")} )} diff --git a/app/features/settings/components/PreferencesTab.tsx b/app/features/settings/components/PreferencesTab.tsx new file mode 100644 index 000000000..3225c09bf --- /dev/null +++ b/app/features/settings/components/PreferencesTab.tsx @@ -0,0 +1,150 @@ +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { Divider } from "~/components/Divider"; +import { SendouButton } from "~/components/elements/Button"; +import { SendouPopover } from "~/components/elements/Popover"; +import { FormMessage } from "~/components/FormMessage"; +import { Label } from "~/components/Label"; +import { useUser } from "~/features/auth/core/user"; +import { SendouForm } from "~/form/SendouForm"; +import { + disableBuildAbilitySortingSchema, + disallowScrimPickupsFromUntrustedSchema, + spoilerFreeModeSchema, +} from "../settings-schemas"; + +export function PreferencesTab() { + const user = useUser(); + if (!user) return null; + + return ( +
+ + +
+ + {({ FormField }) => } + + + {({ FormField }) => } + + + {({ FormField }) => } + +
+
+ ); +} + +function PushNotificationsEnabler() { + const { t } = useTranslation(["common"]); + const [notificationsPermsGranted, setNotificationsPermsGranted] = + React.useState("default"); + + React.useEffect(() => { + if (!("serviceWorker" in navigator)) { + setNotificationsPermsGranted("not-supported"); + return; + } + + if (!("PushManager" in window)) { + setNotificationsPermsGranted("not-supported"); + return; + } + + setNotificationsPermsGranted(Notification.permission); + }, []); + + function askPermission() { + Notification.requestPermission().then((permission) => { + setNotificationsPermsGranted(permission); + if (permission === "granted") { + initServiceWorker(); + } + }); + } + + async function initServiceWorker() { + const swRegistration = await navigator.serviceWorker.register("sw-2.js"); + const subscription = await swRegistration.pushManager.getSubscription(); + if (subscription) { + sendSubscriptionToServer(subscription); + } else { + const subscription = await swRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: import.meta.env.VITE_VAPID_PUBLIC_KEY, + }); + sendSubscriptionToServer(subscription); + } + } + + function sendSubscriptionToServer(subscription: PushSubscription) { + fetch("/notifications/subscribe", { + method: "post", + body: JSON.stringify(subscription), + headers: { "content-type": "application/json" }, + }); + } + + return ( +
+ + {notificationsPermsGranted === "granted" ? ( + + {t("common:actions.disable")} + + } + > + {t("common:settings.notifications.disableInfo")} + + ) : notificationsPermsGranted === "not-supported" || + notificationsPermsGranted === "denied" ? ( + + {t("common:actions.enable")} + + } + > + {notificationsPermsGranted === "not-supported" + ? t("common:settings.notifications.browserNotSupported") + : t("common:settings.notifications.permissionDenied")} + + ) : ( + + {t("common:actions.enable")} + + )} + + {t("common:settings.notifications.description")} + +
+ ); +} diff --git a/app/features/settings/components/SoundsTab.module.css b/app/features/settings/components/SoundsTab.module.css new file mode 100644 index 000000000..053247568 --- /dev/null +++ b/app/features/settings/components/SoundsTab.module.css @@ -0,0 +1,10 @@ +.volumeSliderIcon { + width: 16px; + height: 16px; +} + +.volumeSliderInput { + padding-left: 0 !important; + padding-right: 0 !important; + border: 0 !important; +} diff --git a/app/features/settings/components/SoundsTab.tsx b/app/features/settings/components/SoundsTab.tsx new file mode 100644 index 000000000..cf5d9d175 --- /dev/null +++ b/app/features/settings/components/SoundsTab.tsx @@ -0,0 +1,106 @@ +import { Volume2 } from "lucide-react"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { + soundCodeToLocalStorageKey, + soundVolume, +} from "~/features/chat/chat-utils"; +import { useHydrated } from "~/hooks/useHydrated"; +import { soundPath } from "~/utils/urls"; +import styles from "./SoundsTab.module.css"; + +export function SoundsTab() { + const isHydrated = useHydrated(); + + return ( +
+ {isHydrated ? : null} + {isHydrated ? : null} +
+ ); +} + +function SoundCheckboxes() { + const { t } = useTranslation(["settings"]); + + const sounds = [ + { code: "sq_like", name: t("settings:sounds.likeReceived") }, + { code: "sq_new-group", name: t("settings:sounds.groupNewMember") }, + { code: "sq_match", name: t("settings:sounds.matchStarted") }, + { + code: "tournament_match", + name: t("settings:sounds.tournamentMatchStarted"), + }, + ]; + + const currentValue = (code: string) => + !localStorage.getItem(soundCodeToLocalStorageKey(code)) || + localStorage.getItem(soundCodeToLocalStorageKey(code)) === "true"; + + const [soundValues, setSoundValues] = React.useState( + Object.fromEntries( + sounds.map((sound) => [sound.code, currentValue(sound.code)]), + ), + ); + + const toggleSound = (code: string) => { + localStorage.setItem( + soundCodeToLocalStorageKey(code), + String(!currentValue(code)), + ); + setSoundValues((prev) => ({ + ...prev, + [code]: !prev[code], + })); + }; + + return ( +
+ {sounds.map((sound) => ( +
+ +
+ ))} +
+ ); +} + +function SoundSlider() { + const [volume, setVolume] = React.useState(() => soundVolume() || 100); + + const changeVolume = (event: React.ChangeEvent) => { + const newVolume = Number.parseFloat(event.target.value); + setVolume(newVolume); + localStorage.setItem( + "settings__sound-volume", + String(Math.floor(newVolume)), + ); + }; + + const playSound = () => { + const audio = new Audio(soundPath("sq_like")); + audio.volume = soundVolume() / 100; + void audio.play(); + }; + + return ( +
+ + +
+ ); +} diff --git a/app/features/settings/components/ThemeTab.tsx b/app/features/settings/components/ThemeTab.tsx new file mode 100644 index 000000000..42842b6d7 --- /dev/null +++ b/app/features/settings/components/ThemeTab.tsx @@ -0,0 +1,83 @@ +import { useTranslation } from "react-i18next"; +import { useFetcher, useMatches } from "react-router"; +import { CustomThemeSelector } from "~/components/CustomThemeSelector"; +import { FormMessage } from "~/components/FormMessage"; +import { Theme, useTheme } from "~/features/theme/core/provider"; +import { SelectFormField } from "~/form/fields/SelectFormField"; +import { useHasRole } from "~/modules/permissions/hooks"; +import type { RootLoaderData } from "~/root"; +import type { ThemeInput } from "~/utils/oklch-gamut"; + +export function ThemeTab() { + const { t } = useTranslation(["common"]); + + return ( +
+ + + {t("common:settings.themeInfo")} +
+ ); +} + +function ThemeSelector() { + const { t } = useTranslation(["common"]); + const { userTheme, setUserTheme } = useTheme(); + + const themeItems = (["auto", Theme.DARK, Theme.LIGHT] as const).map( + (theme) => ({ + value: theme, + label: t(`common:theme.${theme}`), + }), + ); + + const handleThemeChange = (newTheme: string | null) => { + if (!newTheme) return; + setUserTheme(newTheme as Theme); + }; + + return ( + + ); +} + +function CustomColorSelector() { + const [root] = useMatches(); + const rootData = root.data as RootLoaderData | undefined; + const isSupporter = useHasRole("SUPPORTER"); + const fetcher = useFetcher(); + + const handleSave = (themeInput: ThemeInput) => { + fetcher.submit( + { + _action: "UPDATE_CUSTOM_THEME", + newValue: themeInput, + revalidateRoot: true, + } as unknown as Parameters[0], + { method: "post", encType: "application/json" }, + ); + }; + + const handleReset = () => { + fetcher.submit( + { _action: "UPDATE_CUSTOM_THEME", newValue: null, revalidateRoot: true }, + { method: "post", encType: "application/json" }, + ); + }; + + return ( + + ); +} diff --git a/app/features/settings/loaders/settings.server.ts b/app/features/settings/loaders/settings.server.ts index 15e913061..08a8b3b69 100644 --- a/app/features/settings/loaders/settings.server.ts +++ b/app/features/settings/loaders/settings.server.ts @@ -1,15 +1,15 @@ +import type { LoaderFunctionArgs } from "react-router"; import { getUser } from "~/features/auth/core/user.server"; -import * as UserRepository from "~/features/user-page/UserRepository.server"; +import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; -export const loader = async () => { +export const loader = async (_args: LoaderFunctionArgs) => { const user = getUser(); - return { - noScreen: user - ? await UserRepository.anyUserPrefersNoScreen([user.id]) - : null, - noSplatnet: user - ? await UserRepository.anyUserPrefersNoSplatnet([user.id]) - : null, - }; + if (!user) { + return { matchProfile: null }; + } + + const matchProfile = await MatchProfileRepository.settingsByUserId(user.id); + + return { matchProfile }; }; diff --git a/app/features/settings/match-profile-schemas.ts b/app/features/settings/match-profile-schemas.ts new file mode 100644 index 000000000..381a327f6 --- /dev/null +++ b/app/features/settings/match-profile-schemas.ts @@ -0,0 +1,74 @@ +import { z } from "zod"; +import type { UserMapModePreferences } from "~/db/tables"; +import { + checkboxGroup, + customField, + radioGroup, + stringConstant, + toggle, + weaponPool, +} from "~/form/fields"; +import { languagesUnified } from "~/modules/i18n/config"; +import { modeShort, stageId } from "~/utils/zod"; +import { + AMOUNT_OF_MAPS_IN_POOL_PER_MODE, + MATCH_PROFILE_WEAPON_POOL_MAX_SIZE, +} from "../match-profile/match-profile-constants"; + +export const LANGUAGE_OPTIONS = languagesUnified.map((lang) => ({ + label: () => lang.name, + value: lang.code, +})); + +const preferenceSchema = z.enum(["AVOID", "PREFER"]).optional(); + +const mapModePreferencesValueSchema = z + .object({ + modes: z.array(z.object({ mode: modeShort, preference: preferenceSchema })), + pool: z.array( + z.object({ + stages: z.array(stageId).max(AMOUNT_OF_MAPS_IN_POOL_PER_MODE), + mode: modeShort, + }), + ), + }) + .refine( + (val) => + val.pool.every((pool) => { + const mp = val.modes.find((m) => m.mode === pool.mode); + return mp?.preference !== "AVOID"; + }), + "Can't have map pool for a mode that was avoided", + ); + +export const updateMatchProfileSchema = z.object({ + _action: stringConstant("UPDATE_MATCH_PROFILE"), + mapModePreferences: customField( + { initialValue: { modes: [], pool: [] } satisfies UserMapModePreferences }, + mapModePreferencesValueSchema, + ), + weaponPool: weaponPool({ + label: "labels.weaponPool", + maxCount: MATCH_PROFILE_WEAPON_POOL_MAX_SIZE, + }), + vc: radioGroup({ + label: "labels.voiceChat", + items: [ + { label: "options.voiceChat.yes", value: "YES" }, + { label: "options.voiceChat.no", value: "NO" }, + { label: "options.voiceChat.listenOnly", value: "LISTEN_ONLY" }, + ], + }), + languages: checkboxGroup({ + label: "labels.languages", + items: LANGUAGE_OPTIONS, + }), + noSplatnet: toggle({ + label: "labels.noSplatnet", + bottomText: "bottomTexts.noScreen", + }), + noScreen: toggle({ + label: "labels.noScreen", + bottomText: "bottomTexts.noScreen", + }), +}); diff --git a/app/features/settings/routes/settings.module.css b/app/features/settings/routes/settings.module.css deleted file mode 100644 index a627cda46..000000000 --- a/app/features/settings/routes/settings.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.divider { - margin-top: var(--s-4); - margin-bottom: calc(var(--s-2) * -1); -} diff --git a/app/features/settings/routes/settings.tsx b/app/features/settings/routes/settings.tsx index f924f9427..00b563f53 100644 --- a/app/features/settings/routes/settings.tsx +++ b/app/features/settings/routes/settings.tsx @@ -1,48 +1,41 @@ -import { LogOut } from "lucide-react"; -import * as React from "react"; +import { + Globe, + LogOut, + Map as MapIcon, + Palette, + SlidersHorizontal, + Volume2, +} from "lucide-react"; import { useTranslation } from "react-i18next"; import type { MetaFunction } from "react-router"; -import { - useFetcher, - useLoaderData, - useMatches, - useNavigate, - useSearchParams, -} from "react-router"; -import { CustomThemeSelector } from "~/components/CustomThemeSelector"; -import { Divider } from "~/components/Divider"; -import { FormMessage } from "~/components/FormMessage"; -import { Label } from "~/components/Label"; +import { useSearchParams } from "react-router"; import { Main } from "~/components/Main"; import { useUser } from "~/features/auth/core/user"; -import { Theme, useTheme } from "~/features/theme/core/provider"; -import { SelectFormField } from "~/form/fields/SelectFormField"; -import { SendouForm } from "~/form/SendouForm"; -import { languages } from "~/modules/i18n/config"; -import { useHasRole } from "~/modules/permissions/hooks"; -import type { RootLoaderData } from "~/root"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { LOG_OUT_URL, navIconUrl, SETTINGS_PAGE } from "~/utils/urls"; import { SendouButton } from "../../../components/elements/Button"; -import { SendouPopover } from "../../../components/elements/Popover"; -import { action } from "../actions/settings.server"; -import { loader } from "../loaders/settings.server"; import { - clockFormatSchema, - disableBuildAbilitySortingSchema, - disallowScrimPickupsFromUntrustedSchema, - spoilerFreeModeSchema, - updateNoScreenSchema, - updateNoSplatnetSchema, -} from "../settings-schemas"; -import styles from "./settings.module.css"; + SendouTab, + SendouTabList, + SendouTabPanel, + SendouTabs, +} from "../../../components/elements/Tabs"; +import { action } from "../actions/settings.server"; +import { LocaleTab } from "../components/LocaleTab"; +import { MatchProfileTab } from "../components/MatchProfileTab"; +import { PreferencesTab } from "../components/PreferencesTab"; +import { SoundsTab } from "../components/SoundsTab"; +import { ThemeTab } from "../components/ThemeTab"; +import { loader } from "../loaders/settings.server"; +import type { SettingsTabSlug } from "../settings-constants"; +import { defaultTab, resolveActiveTab } from "../settings-utils"; import "./settings.global.css"; -import type { ThemeInput } from "~/utils/oklch-gamut"; export { action, loader }; export const handle: SendouRouteHandle = { + i18n: ["settings"], breadcrumb: () => ({ imgPath: navIconUrl("settings"), href: SETTINGS_PAGE, @@ -50,15 +43,38 @@ export const handle: SendouRouteHandle = { }), }; +export const meta: MetaFunction = (args) => { + return metaTags({ + title: "Settings", + location: args.location, + }); +}; + export default function SettingsPage() { - const data = useLoaderData(); const user = useUser(); - const { t } = useTranslation(["common"]); + const { t } = useTranslation(["common", "settings"]); + const [searchParams, setSearchParams] = useSearchParams(); + + const isLoggedIn = Boolean(user); + const activeTab = resolveActiveTab(searchParams.get("tab"), isLoggedIn); + + const handleSelectionChange = (key: React.Key) => { + const slug = key as SettingsTabSlug; + const next = new URLSearchParams(searchParams); + if (slug === defaultTab(isLoggedIn)) { + next.delete("tab"); + } else { + next.set("tab", slug); + } + setSearchParams(next, { + defaultShouldRevalidate: false, + }); + }; return ( -
+
-
+

{t("common:pages.settings")}

{user ? (
@@ -73,289 +89,58 @@ export default function SettingsPage() {
) : null}
- - {t("common:settings.locales")} - - - {user ? ( - - {({ FormField }) => } - - ) : null} - {user ? ( - <> - - {t("common:settings.preferences")} - - -
- - {({ FormField }) => } - - - {({ FormField }) => } - - - {({ FormField }) => } - - - {({ FormField }) => } - - - {({ FormField }) => } - -
- - ) : null} - - {t("common:settings.theme")} - - - - {t("common:settings.themeInfo")} + + + {user ? ( + }> + {t("settings:tabs.matchProfile")} + + ) : null} + {user ? ( + }> + {t("settings:tabs.preferences")} + + ) : null} + }> + {t("settings:tabs.locale")} + + }> + {t("settings:tabs.theme")} + + {user ? ( + }> + {t("settings:tabs.sounds")} + + ) : null} + + {user ? ( + + + + ) : null} + {user ? ( + + + + ) : null} + + + + + + + {user ? ( + + + + ) : null} +
); } - -export const meta: MetaFunction = (args) => { - return metaTags({ - title: "Settings", - location: args.location, - }); -}; - -function LanguageSelector() { - const { t } = useTranslation(["common"]); - const { i18n } = useTranslation(); - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - const languageItems = languages.map((lang) => ({ - value: lang.code, - label: lang.name, - })); - - const handleLanguageChange = (newLang: string | null) => { - if (!newLang) return; - navigate(`?${addUniqueParam(searchParams, "lng", newLang).toString()}`); - }; - - return ( - - ); -} - -function addUniqueParam( - oldParams: URLSearchParams, - name: string, - value: string, -): URLSearchParams { - const paramsCopy = new URLSearchParams(oldParams); - paramsCopy.delete(name); - paramsCopy.append(name, value); - return paramsCopy; -} - -function ThemeSelector() { - const { t } = useTranslation(["common"]); - const { userTheme, setUserTheme } = useTheme(); - - const themeItems = (["auto", Theme.DARK, Theme.LIGHT] as const).map( - (theme) => ({ - value: theme, - label: t(`common:theme.${theme}`), - }), - ); - - const handleThemeChange = (newTheme: string | null) => { - if (!newTheme) return; - setUserTheme(newTheme as Theme); - }; - - return ( - - ); -} - -function CustomColorSelector() { - const [root] = useMatches(); - const rootData = root.data as RootLoaderData | undefined; - const isSupporter = useHasRole("SUPPORTER"); - const fetcher = useFetcher(); - - const handleSave = (themeInput: ThemeInput) => { - fetcher.submit( - { - _action: "UPDATE_CUSTOM_THEME", - newValue: themeInput, - revalidateRoot: true, - } as unknown as Parameters[0], - { method: "post", encType: "application/json" }, - ); - }; - - const handleReset = () => { - fetcher.submit( - { _action: "UPDATE_CUSTOM_THEME", newValue: null, revalidateRoot: true }, - { method: "post", encType: "application/json" }, - ); - }; - - return ( - - ); -} - -// adapted from https://pqvst.com/2023/11/21/web-push-notifications/ -function PushNotificationsEnabler() { - const { t } = useTranslation(["common"]); - const [notificationsPermsGranted, setNotificationsPermsGranted] = - React.useState("default"); - - React.useEffect(() => { - if (!("serviceWorker" in navigator)) { - // Service Worker isn't supported on this browser, disable or hide UI. - setNotificationsPermsGranted("not-supported"); - return; - } - - if (!("PushManager" in window)) { - // Push isn't supported on this browser, disable or hide UI. - setNotificationsPermsGranted("not-supported"); - return; - } - - setNotificationsPermsGranted(Notification.permission); - }, []); - - function askPermission() { - Notification.requestPermission().then((permission) => { - setNotificationsPermsGranted(permission); - if (permission === "granted") { - initServiceWorker(); - } - }); - } - - async function initServiceWorker() { - const swRegistration = await navigator.serviceWorker.register("sw-2.js"); - const subscription = await swRegistration.pushManager.getSubscription(); - if (subscription) { - sendSubscriptionToServer(subscription); - } else { - const subscription = await swRegistration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: import.meta.env.VITE_VAPID_PUBLIC_KEY, - }); - sendSubscriptionToServer(subscription); - } - } - - function sendSubscriptionToServer(subscription: PushSubscription) { - fetch("/notifications/subscribe", { - method: "post", - body: JSON.stringify(subscription), - headers: { "content-type": "application/json" }, - }); - } - - return ( -
- - {notificationsPermsGranted === "granted" ? ( - - {t("common:actions.disable")} - - } - > - {t("common:settings.notifications.disableInfo")} - - ) : notificationsPermsGranted === "not-supported" || - notificationsPermsGranted === "denied" ? ( - - {t("common:actions.enable")} - - } - > - {notificationsPermsGranted === "not-supported" - ? t("common:settings.notifications.browserNotSupported") - : t("common:settings.notifications.permissionDenied")} - - ) : ( - - {t("common:actions.enable")} - - )} - - {t("common:settings.notifications.description")} - -
- ); -} diff --git a/app/features/settings/settings-constants.ts b/app/features/settings/settings-constants.ts new file mode 100644 index 000000000..ce8fd9e6e --- /dev/null +++ b/app/features/settings/settings-constants.ts @@ -0,0 +1,9 @@ +export const SETTINGS_TAB_SLUGS = [ + "preferences", + "match-profile", + "locale", + "theme", + "sounds", +] as const; + +export type SettingsTabSlug = (typeof SETTINGS_TAB_SLUGS)[number]; diff --git a/app/features/settings/settings-schemas.server.ts b/app/features/settings/settings-schemas.server.ts new file mode 100644 index 000000000..ddd072ddd --- /dev/null +++ b/app/features/settings/settings-schemas.server.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +import { updateMatchProfileSchema } from "./match-profile-schemas"; +import { settingsEditSchema } from "./settings-schemas"; + +export const settingsActionSchema = z.union([ + settingsEditSchema, + updateMatchProfileSchema, +]); diff --git a/app/features/settings/settings-schemas.ts b/app/features/settings/settings-schemas.ts index b8fd0b0db..538a8380a 100644 --- a/app/features/settings/settings-schemas.ts +++ b/app/features/settings/settings-schemas.ts @@ -43,22 +43,6 @@ export const spoilerFreeModeSchema = z.object({ }), }); -export const updateNoScreenSchema = z.object({ - _action: stringConstant("UPDATE_NO_SCREEN"), - newValue: toggle({ - label: "labels.noScreen", - bottomText: "bottomTexts.noScreen", - }), -}); - -export const updateNoSplatnetSchema = z.object({ - _action: stringConstant("UPDATE_NO_SPLATNET"), - newValue: toggle({ - label: "labels.noSplatnet", - bottomText: "bottomTexts.noScreen", - }), -}); - const weaponReportDefaultOpenSchema = z.object({ _action: stringConstant("UPDATE_WEAPON_REPORT_DEFAULT_OPEN"), newValue: z.boolean(), @@ -69,8 +53,6 @@ export const settingsEditSchema = z.union([ disableBuildAbilitySortingSchema, disallowScrimPickupsFromUntrustedSchema, spoilerFreeModeSchema, - updateNoScreenSchema, - updateNoSplatnetSchema, clockFormatSchema, weaponReportDefaultOpenSchema, ]); diff --git a/app/features/settings/settings-utils.ts b/app/features/settings/settings-utils.ts new file mode 100644 index 000000000..293a31303 --- /dev/null +++ b/app/features/settings/settings-utils.ts @@ -0,0 +1,19 @@ +import { SETTINGS_TAB_SLUGS, type SettingsTabSlug } from "./settings-constants"; + +const PUBLIC_TABS = new Set(["locale", "theme"]); + +export function defaultTab(isLoggedIn: boolean): SettingsTabSlug { + return isLoggedIn ? "match-profile" : "theme"; +} + +export function resolveActiveTab( + raw: string | null, + isLoggedIn: boolean, +): SettingsTabSlug { + if (raw && (SETTINGS_TAB_SLUGS as readonly string[]).includes(raw)) { + const slug = raw as SettingsTabSlug; + if (!isLoggedIn && !PUBLIC_TABS.has(slug)) return defaultTab(false); + return slug; + } + return defaultTab(isLoggedIn); +} diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.ts index 26fd51415..375aa8cdc 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.ts @@ -66,7 +66,7 @@ type TournamentLFGMemberObject = { pronouns: Tables["User"]["pronouns"]; role: Tables["TournamentTeamMember"]["role"]; isStayAsSub: Tables["TournamentTeamMember"]["isStayAsSub"]; - weapons: Tables["User"]["qWeaponPool"]; + weapons: Tables["User"]["weaponPool"]; plusTier: Tables["PlusTier"]["tier"] | null; }; @@ -106,7 +106,7 @@ export async function findLookingTeamsByTournamentId(tournamentId: number) { pronouns: eb.ref("User.pronouns"), role: eb.ref("TournamentTeamMember.role"), isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"), - weapons: eb.ref("User.qWeaponPool"), + weapons: eb.ref("User.weaponPool"), plusTier: eb.ref("PlusTier.tier"), }), ]) @@ -145,7 +145,7 @@ export async function findSubGroups(tournamentId: number) { pronouns: eb.ref("User.pronouns"), role: eb.ref("TournamentTeamMember.role"), isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"), - weapons: eb.ref("User.qWeaponPool"), + weapons: eb.ref("User.weaponPool"), plusTier: eb.ref("PlusTier.tier"), }), ]) diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index afb73af83..1db01a842 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -43,7 +43,7 @@ import TimePopover from "~/components/TimePopover"; import { useUser } from "~/features/auth/core/user"; import { imgTypeToDimensions } from "~/features/img-upload/upload-constants"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; -import { ModeMapPoolPicker } from "~/features/sendouq-settings/components/ModeMapPoolPicker"; +import { ModeMapPoolPicker } from "~/features/settings/components/ModeMapPoolPicker"; import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useAutoRerender } from "~/hooks/useAutoRerender"; diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts index 3e0a0bb6c..8868bf382 100644 --- a/app/features/tournament/tournament-utils.ts +++ b/app/features/tournament/tournament-utils.ts @@ -11,8 +11,8 @@ import type { } from "../../db/tables"; import { assertUnreachable } from "../../utils/types"; import { MapPool } from "../map-list-generator/core/map-pool"; +import { BANNED_MAPS } from "../match-profile/banned-maps"; import * as Seasons from "../mmr/core/Seasons"; -import { BANNED_MAPS } from "../sendouq-settings/banned-maps"; import type { ParsedBracket } from "../tournament-bracket/core/Progression"; import * as Progression from "../tournament-bracket/core/Progression"; import type { Tournament as TournamentClass } from "../tournament-bracket/core/Tournament"; diff --git a/app/form/SendouForm.module.css b/app/form/SendouForm.module.css index 3088885fa..10b675290 100644 --- a/app/form/SendouForm.module.css +++ b/app/form/SendouForm.module.css @@ -1,10 +1,15 @@ .form { display: flex; flex-direction: column; - gap: var(--s-4); + gap: var(--s-6); width: 100%; max-width: 24rem; margin: 0 auto; + + &.fullWidth { + margin: 0; + max-width: none; + } } .title { diff --git a/app/form/SendouForm.tsx b/app/form/SendouForm.tsx index b0b6d47c6..636c1228f 100644 --- a/app/form/SendouForm.tsx +++ b/app/form/SendouForm.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import * as React from "react"; import { flushSync } from "react-dom"; import { useTranslation } from "react-i18next"; @@ -63,6 +64,12 @@ type BaseFormProps = { autoApply?: boolean; revalidateRoot?: boolean; className?: string; + /** + * When true, opts out of the default centered, max-width layout so the form + * expands to fill its parent container. Use when embedding a form inside a + * layout that already controls width/alignment. + */ + fullWidth?: boolean; onApply?: (values: z.infer>) => void; secondarySubmit?: React.ReactNode; }; @@ -89,6 +96,7 @@ export function SendouForm({ autoApply, revalidateRoot, className, + fullWidth, onApply, secondarySubmit, }: SendouFormProps) { @@ -388,15 +396,18 @@ export function SendouForm({ ); + const resolvedClassName = + className ?? clsx(styles.form, { [styles.fullWidth]: fullWidth }); + return ( {autoApply && onApply ? ( -
{formContent}
+
{formContent}
) : (
{formContent} diff --git a/app/modules/i18n/resources.browser.ts b/app/modules/i18n/resources.browser.ts index da64a2b56..e4a47dd69 100644 --- a/app/modules/i18n/resources.browser.ts +++ b/app/modules/i18n/resources.browser.ts @@ -15,6 +15,7 @@ import lfg from "../../../locales/en/lfg.json"; import org from "../../../locales/en/org.json"; import q from "../../../locales/en/q.json"; import scrims from "../../../locales/en/scrims.json"; +import settings from "../../../locales/en/settings.json"; import team from "../../../locales/en/team.json"; import tierListMaker from "../../../locales/en/tier-list-maker.json"; import tournament from "../../../locales/en/tournament.json"; @@ -41,6 +42,7 @@ export const resources = { org, q, scrims, + settings, team, "tier-list-maker": tierListMaker, tournament, diff --git a/app/modules/i18n/resources.server.ts b/app/modules/i18n/resources.server.ts index 376bf12ae..4a30761b3 100644 --- a/app/modules/i18n/resources.server.ts +++ b/app/modules/i18n/resources.server.ts @@ -16,6 +16,7 @@ import lfgDa from "../../../locales/da/lfg.json"; import orgDa from "../../../locales/da/org.json"; import qDa from "../../../locales/da/q.json"; import scrimsDa from "../../../locales/da/scrims.json"; +import settingsDa from "../../../locales/da/settings.json"; import teamDa from "../../../locales/da/team.json"; import tierListMakerDa from "../../../locales/da/tier-list-maker.json"; import tournamentDa from "../../../locales/da/tournament.json"; @@ -40,6 +41,7 @@ import lfgDe from "../../../locales/de/lfg.json"; import orgDe from "../../../locales/de/org.json"; import qDe from "../../../locales/de/q.json"; import scrimsDe from "../../../locales/de/scrims.json"; +import settingsDe from "../../../locales/de/settings.json"; import teamDe from "../../../locales/de/team.json"; import tierListMakerDe from "../../../locales/de/tier-list-maker.json"; import tournamentDe from "../../../locales/de/tournament.json"; @@ -64,6 +66,7 @@ import lfg from "../../../locales/en/lfg.json"; import org from "../../../locales/en/org.json"; import q from "../../../locales/en/q.json"; import scrimsEn from "../../../locales/en/scrims.json"; +import settings from "../../../locales/en/settings.json"; import team from "../../../locales/en/team.json"; import tierListMaker from "../../../locales/en/tier-list-maker.json"; import tournament from "../../../locales/en/tournament.json"; @@ -88,6 +91,7 @@ import lfgEsEs from "../../../locales/es-ES/lfg.json"; import orgEsEs from "../../../locales/es-ES/org.json"; import qEsEs from "../../../locales/es-ES/q.json"; import scrimsEsEs from "../../../locales/es-ES/scrims.json"; +import settingsEsEs from "../../../locales/es-ES/settings.json"; import teamEsEs from "../../../locales/es-ES/team.json"; import tierListMakerEsEs from "../../../locales/es-ES/tier-list-maker.json"; import tournamentEsEs from "../../../locales/es-ES/tournament.json"; @@ -112,6 +116,7 @@ import lfgEsUs from "../../../locales/es-US/lfg.json"; import orgEsUs from "../../../locales/es-US/org.json"; import qEsUs from "../../../locales/es-US/q.json"; import scrimsEsUs from "../../../locales/es-US/scrims.json"; +import settingsEsUs from "../../../locales/es-US/settings.json"; import teamEsUs from "../../../locales/es-US/team.json"; import tierListMakerEsUs from "../../../locales/es-US/tier-list-maker.json"; import tournamentEsUs from "../../../locales/es-US/tournament.json"; @@ -136,6 +141,7 @@ import lfgFrCa from "../../../locales/fr-CA/lfg.json"; import orgFrCa from "../../../locales/fr-CA/org.json"; import qFrCa from "../../../locales/fr-CA/q.json"; import scrimsFrCa from "../../../locales/fr-CA/scrims.json"; +import settingsFrCa from "../../../locales/fr-CA/settings.json"; import teamFrCa from "../../../locales/fr-CA/team.json"; import tierListMakerFrCa from "../../../locales/fr-CA/tier-list-maker.json"; import tournamentFrCa from "../../../locales/fr-CA/tournament.json"; @@ -160,6 +166,7 @@ import lfgFrEu from "../../../locales/fr-EU/lfg.json"; import orgFrEu from "../../../locales/fr-EU/org.json"; import qFrEu from "../../../locales/fr-EU/q.json"; import scrimsFrEu from "../../../locales/fr-EU/scrims.json"; +import settingsFrEu from "../../../locales/fr-EU/settings.json"; import teamFrEu from "../../../locales/fr-EU/team.json"; import tierListMakerFrEu from "../../../locales/fr-EU/tier-list-maker.json"; import tournamentFrEu from "../../../locales/fr-EU/tournament.json"; @@ -184,6 +191,7 @@ import lfgHe from "../../../locales/he/lfg.json"; import orgHe from "../../../locales/he/org.json"; import qHe from "../../../locales/he/q.json"; import scrimsHe from "../../../locales/he/scrims.json"; +import settingsHe from "../../../locales/he/settings.json"; import teamHe from "../../../locales/he/team.json"; import tierListMakerHe from "../../../locales/he/tier-list-maker.json"; import tournamentHe from "../../../locales/he/tournament.json"; @@ -208,6 +216,7 @@ import lfgIt from "../../../locales/it/lfg.json"; import orgIt from "../../../locales/it/org.json"; import qIt from "../../../locales/it/q.json"; import scrimsIt from "../../../locales/it/scrims.json"; +import settingsIt from "../../../locales/it/settings.json"; import teamIt from "../../../locales/it/team.json"; import tierListMakerIt from "../../../locales/it/tier-list-maker.json"; import tournamentIt from "../../../locales/it/tournament.json"; @@ -232,6 +241,7 @@ import lfgJa from "../../../locales/ja/lfg.json"; import orgJa from "../../../locales/ja/org.json"; import qJa from "../../../locales/ja/q.json"; import scrimsJa from "../../../locales/ja/scrims.json"; +import settingsJa from "../../../locales/ja/settings.json"; import teamJa from "../../../locales/ja/team.json"; import tierListMakerJa from "../../../locales/ja/tier-list-maker.json"; import tournamentJa from "../../../locales/ja/tournament.json"; @@ -256,6 +266,7 @@ import lfgKo from "../../../locales/ko/lfg.json"; import orgKo from "../../../locales/ko/org.json"; import qKo from "../../../locales/ko/q.json"; import scrimsKo from "../../../locales/ko/scrims.json"; +import settingsKo from "../../../locales/ko/settings.json"; import teamKo from "../../../locales/ko/team.json"; import tierListMakerKo from "../../../locales/ko/tier-list-maker.json"; import tournamentKo from "../../../locales/ko/tournament.json"; @@ -280,6 +291,7 @@ import lfgNl from "../../../locales/nl/lfg.json"; import orgNl from "../../../locales/nl/org.json"; import qNl from "../../../locales/nl/q.json"; import scrimsNl from "../../../locales/nl/scrims.json"; +import settingsNl from "../../../locales/nl/settings.json"; import teamNl from "../../../locales/nl/team.json"; import tierListMakerNl from "../../../locales/nl/tier-list-maker.json"; import tournamentNl from "../../../locales/nl/tournament.json"; @@ -304,6 +316,7 @@ import lfgPl from "../../../locales/pl/lfg.json"; import orgPl from "../../../locales/pl/org.json"; import qPl from "../../../locales/pl/q.json"; import scrimsPl from "../../../locales/pl/scrims.json"; +import settingsPl from "../../../locales/pl/settings.json"; import teamPl from "../../../locales/pl/team.json"; import tierListMakerPl from "../../../locales/pl/tier-list-maker.json"; import tournamentPl from "../../../locales/pl/tournament.json"; @@ -328,6 +341,7 @@ import lfgPtBr from "../../../locales/pt-BR/lfg.json"; import orgPtBr from "../../../locales/pt-BR/org.json"; import qPtBr from "../../../locales/pt-BR/q.json"; import scrimsPtBr from "../../../locales/pt-BR/scrims.json"; +import settingsPtBr from "../../../locales/pt-BR/settings.json"; import teamPtBr from "../../../locales/pt-BR/team.json"; import tierListMakerPtBr from "../../../locales/pt-BR/tier-list-maker.json"; import tournamentPtBr from "../../../locales/pt-BR/tournament.json"; @@ -352,6 +366,7 @@ import lfgRu from "../../../locales/ru/lfg.json"; import orgRu from "../../../locales/ru/org.json"; import qRu from "../../../locales/ru/q.json"; import scrimsRu from "../../../locales/ru/scrims.json"; +import settingsRu from "../../../locales/ru/settings.json"; import teamRu from "../../../locales/ru/team.json"; import tierListMakerRu from "../../../locales/ru/tier-list-maker.json"; import tournamentRu from "../../../locales/ru/tournament.json"; @@ -376,6 +391,7 @@ import lfgZh from "../../../locales/zh/lfg.json"; import orgZh from "../../../locales/zh/org.json"; import qZh from "../../../locales/zh/q.json"; import scrimsZh from "../../../locales/zh/scrims.json"; +import settingsZh from "../../../locales/zh/settings.json"; import teamZh from "../../../locales/zh/team.json"; import tierListMakerZh from "../../../locales/zh/tier-list-maker.json"; import tournamentZh from "../../../locales/zh/tournament.json"; @@ -391,6 +407,7 @@ export const resources = { friends: friendsEsUs, weapons: weaponsEsUs, scrims: scrimsEsUs, + settings: settingsEsUs, common: commonEsUs, "game-badges": gameBadgesEsUs, "game-misc": gameMiscEsUs, @@ -417,6 +434,7 @@ export const resources = { friends: friends, weapons: weapons, scrims: scrimsEn, + settings: settings, common: common, "game-badges": gameBadges, "game-misc": gameMisc, @@ -443,6 +461,7 @@ export const resources = { friends: friendsKo, weapons: weaponsKo, scrims: scrimsKo, + settings: settingsKo, common: commonKo, "game-badges": gameBadgesKo, "game-misc": gameMiscKo, @@ -469,6 +488,7 @@ export const resources = { friends: friendsDe, weapons: weaponsDe, scrims: scrimsDe, + settings: settingsDe, common: commonDe, "game-badges": gameBadgesDe, "game-misc": gameMiscDe, @@ -495,6 +515,7 @@ export const resources = { friends: friendsNl, weapons: weaponsNl, scrims: scrimsNl, + settings: settingsNl, common: commonNl, "game-badges": gameBadgesNl, "game-misc": gameMiscNl, @@ -521,6 +542,7 @@ export const resources = { friends: friendsPtBr, weapons: weaponsPtBr, scrims: scrimsPtBr, + settings: settingsPtBr, common: commonPtBr, "game-badges": gameBadgesPtBr, "game-misc": gameMiscPtBr, @@ -547,6 +569,7 @@ export const resources = { friends: friendsZh, weapons: weaponsZh, scrims: scrimsZh, + settings: settingsZh, common: commonZh, "game-badges": gameBadgesZh, "game-misc": gameMiscZh, @@ -573,6 +596,7 @@ export const resources = { friends: friendsFrCa, weapons: weaponsFrCa, scrims: scrimsFrCa, + settings: settingsFrCa, common: commonFrCa, "game-badges": gameBadgesFrCa, "game-misc": gameMiscFrCa, @@ -599,6 +623,7 @@ export const resources = { friends: friendsRu, weapons: weaponsRu, scrims: scrimsRu, + settings: settingsRu, common: commonRu, "game-badges": gameBadgesRu, "game-misc": gameMiscRu, @@ -625,6 +650,7 @@ export const resources = { friends: friendsIt, weapons: weaponsIt, scrims: scrimsIt, + settings: settingsIt, common: commonIt, "game-badges": gameBadgesIt, "game-misc": gameMiscIt, @@ -651,6 +677,7 @@ export const resources = { friends: friendsJa, weapons: weaponsJa, scrims: scrimsJa, + settings: settingsJa, common: commonJa, "game-badges": gameBadgesJa, "game-misc": gameMiscJa, @@ -677,6 +704,7 @@ export const resources = { friends: friendsDa, weapons: weaponsDa, scrims: scrimsDa, + settings: settingsDa, common: commonDa, "game-badges": gameBadgesDa, "game-misc": gameMiscDa, @@ -703,6 +731,7 @@ export const resources = { friends: friendsEsEs, weapons: weaponsEsEs, scrims: scrimsEsEs, + settings: settingsEsEs, common: commonEsEs, "game-badges": gameBadgesEsEs, "game-misc": gameMiscEsEs, @@ -729,6 +758,7 @@ export const resources = { friends: friendsHe, weapons: weaponsHe, scrims: scrimsHe, + settings: settingsHe, common: commonHe, "game-badges": gameBadgesHe, "game-misc": gameMiscHe, @@ -755,6 +785,7 @@ export const resources = { friends: friendsFrEu, weapons: weaponsFrEu, scrims: scrimsFrEu, + settings: settingsFrEu, common: commonFrEu, "game-badges": gameBadgesFrEu, "game-misc": gameMiscFrEu, @@ -781,6 +812,7 @@ export const resources = { friends: friendsPl, weapons: weaponsPl, scrims: scrimsPl, + settings: settingsPl, common: commonPl, "game-badges": gameBadgesPl, "game-misc": gameMiscPl, diff --git a/app/routes.ts b/app/routes.ts index bba7cc61b..f6e2e3bfa 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -210,7 +210,7 @@ export default [ route("looking", "features/sendouq/routes/q.looking.tsx"), route("preparing", "features/sendouq/routes/q.preparing.tsx"), route("match/:id", "features/sendouq-match/routes/q.match.$id.tsx"), - route("settings", "features/sendouq-settings/routes/q.settings.tsx"), + route("settings", "features/match-profile/routes/q.settings.tsx"), route("streams", "features/sendouq-streams/routes/q.streams.tsx"), ]), route("/play", "features/sendouq/routes/play.ts"), diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts index 67c150b30..dc25b0859 100644 --- a/app/utils/i18n.ts +++ b/app/utils/i18n.ts @@ -28,6 +28,7 @@ const ALL_NAMESPACES = [ "org", "front", "friends", + "settings", ] as const; assertType(); assertType<(typeof ALL_NAMESPACES)[number], Namespace>(); diff --git a/app/utils/urls.ts b/app/utils/urls.ts index 0e360d619..c086555a4 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -112,7 +112,7 @@ export const LINKS_PAGE = "/links"; export const SENDOUQ_PAGE = "/q"; export const SENDOUQ_RULES_PAGE = "/q/rules"; export const SENDOUQ_INFO_PAGE = "/q/info"; -export const SENDOUQ_SETTINGS_PAGE = "/q/settings"; +export const MATCH_PROFILE_PAGE = "/settings?tab=match-profile"; export const SENDOUQ_PREPARING_PAGE = "/q/preparing"; export const SENDOUQ_LOOKING_PAGE = "/q/looking"; export const SENDOUQ_LOOKING_PREVIEW_PAGE = "/q/looking?preview=true"; diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 71c1c353f..73ae08db9 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/e2e/seeds/db-seed-AB_RR.sqlite3 b/e2e/seeds/db-seed-AB_RR.sqlite3 index 28c98f243..de65aceee 100644 Binary files a/e2e/seeds/db-seed-AB_RR.sqlite3 and b/e2e/seeds/db-seed-AB_RR.sqlite3 differ diff --git a/e2e/seeds/db-seed-DEFAULT.sqlite3 b/e2e/seeds/db-seed-DEFAULT.sqlite3 index 51bb2da10..6f971c1ee 100644 Binary files a/e2e/seeds/db-seed-DEFAULT.sqlite3 and b/e2e/seeds/db-seed-DEFAULT.sqlite3 differ diff --git a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 index b2ccf4cda..634e951c4 100644 Binary files a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 and b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 differ diff --git a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 index b1b4756c0..fbf3c9c51 100644 Binary files a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 and b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 index 5240dd83f..84dcb6a5a 100644 Binary files a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 and b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 index 5c6587355..5e4227cff 100644 Binary files a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 and b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 index 7295ec087..49276f862 100644 Binary files a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 and b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 index cd17d3164..1af73702d 100644 Binary files a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 and b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 differ diff --git a/e2e/seeds/db-seed-REG_OPEN.sqlite3 b/e2e/seeds/db-seed-REG_OPEN.sqlite3 index f5d35af8d..3aaa35045 100644 Binary files a/e2e/seeds/db-seed-REG_OPEN.sqlite3 and b/e2e/seeds/db-seed-REG_OPEN.sqlite3 differ diff --git a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 index 8bc4ffdff..c4b8c7bb8 100644 Binary files a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 and b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 differ diff --git a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 index 6e1bb68d0..5385c8c24 100644 Binary files a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 and b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 differ diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index c22f6d5c7..2dde17bb4 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -1,9 +1,11 @@ import type { Page } from "@playwright/test"; import { + clockFormatSchema, disableBuildAbilitySortingSchema, spoilerFreeModeSchema, } from "~/features/settings/settings-schemas"; import { + CALENDAR_PAGE, SETTINGS_PAGE, tournamentBracketsPage, tournamentResultsPage, @@ -36,7 +38,7 @@ test.describe("Settings", () => { await navigate({ page, - url: SETTINGS_PAGE, + url: `${SETTINGS_PAGE}?tab=preferences`, }); const form = createFormHelpers(page, disableBuildAbilitySortingSchema); @@ -55,10 +57,46 @@ test.describe("Settings", () => { expect(newContents).not.toBe(oldContents); }); + + test("updates clock format preference", async ({ page }) => { + await seed(page); + await impersonate(page); + + await navigate({ + page, + url: CALENDAR_PAGE, + }); + + const clockTime = page + .locator("[class*='clockHeader'] [class*='reserve-one-lb']") + .first(); + const initialTime = await clockTime.textContent(); + + expect(initialTime).toMatch(/AM|PM/); + + await navigate({ + page, + url: `${SETTINGS_PAGE}?tab=locale`, + }); + + const form = createFormHelpers(page, clockFormatSchema); + await waitForPOSTResponse(page, () => form.select("newValue", "24h")); + + await navigate({ + page, + url: CALENDAR_PAGE, + }); + + const newTime = await clockTime.textContent(); + + expect(newTime).not.toMatch(/AM|PM/); + expect(newTime).not.toBe(initialTime); + expect(newTime).toContain(":"); + }); }); const enableSpoilerFreeMode = async (page: Page) => { - await navigate({ page, url: SETTINGS_PAGE }); + await navigate({ page, url: `${SETTINGS_PAGE}?tab=preferences` }); const form = createFormHelpers(page, spoilerFreeModeSchema); await waitForPOSTResponse(page, () => form.check("newValue")); }; diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index 0859d77e6..9ff741907 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -1,6 +1,6 @@ import { NZAP_TEST_ID } from "~/db/seed/constants"; import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants"; -import { updateNoScreenSchema } from "~/features/settings/settings-schemas"; +import { updateMatchProfileSchema } from "~/features/settings/match-profile-schemas"; import { NOTIFICATIONS_URL, SETTINGS_PAGE, @@ -828,8 +828,9 @@ test.describe("Tournament bracket", () => { url: SETTINGS_PAGE, }); - const form = createFormHelpers(page, updateNoScreenSchema); - await form.check("newValue"); + const form = createFormHelpers(page, updateMatchProfileSchema); + await form.check("noScreen"); + await waitForPOSTResponse(page, () => form.submit()); await navigate({ page, diff --git a/e2e/tournament.spec.ts b/e2e/tournament.spec.ts index e3c002562..e4498bed0 100644 --- a/e2e/tournament.spec.ts +++ b/e2e/tournament.spec.ts @@ -1,4 +1,4 @@ -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; import type { StageId } from "~/modules/in-game-lists/types"; import { diff --git a/e2e/user-page.spec.ts b/e2e/user-page.spec.ts index 7b396402f..f0e9c2e4c 100644 --- a/e2e/user-page.spec.ts +++ b/e2e/user-page.spec.ts @@ -124,7 +124,7 @@ test.describe("User page", () => { (el) => el.style.getPropertyValue("--_base-h") !== "", ); - await navigate({ page, url: "/settings" }); + await navigate({ page, url: "/settings?tab=theme" }); // initially no custom theme await expect(hasCustomTheme()).resolves.toBe(false); diff --git a/locales/da/q.json b/locales/da/q.json index 09c57a825..03571db99 100644 --- a/locales/da/q.json +++ b/locales/da/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/da/settings.json b/locales/da/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/da/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/de/q.json b/locales/de/q.json index 75631dd93..c80c6b27b 100644 --- a/locales/de/q.json +++ b/locales/de/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/de/settings.json b/locales/de/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/de/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/en/q.json b/locales/en/q.json index 96682db6a..e75fbb1ed 100644 --- a/locales/en/q.json +++ b/locales/en/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Season {{nth}} open", "front.preview": "Preview groups in the queue without joining", "front.preview.explanation": "This feature is only available to Supporter tier (or above) patrons of sendou.ink", - "settings.maps.header": "Stages and modes", - "settings.maps.personal": "Personal", - "settings.maps.preferencesFor": "Preferences for", - "settings.maps.teamExplanation": "Team map list is used for all members when queueing as a team", - "settings.maps.avoid": "Avoid", - "settings.maps.prefer": "Prefer", - "settings.maps.neutral": "Neutral", - "settings.weaponPool.header": "Weapon pool", - "settings.weaponPool.full": "Weapon pool is full", - "settings.voiceChat.header": "Voice chat", - "settings.voiceChat.canVC.header": "Can voice chat?", - "settings.voiceChat.canVC.yes": "Yes", - "settings.voiceChat.canVC.no": "No", - "settings.voiceChat.canVC.listenOnly": "Listen only", - "settings.voiceChat.languages.header": "Your languages", - "settings.voiceChat.languages.placeholder": "Select all that apply", - "settings.sounds.header": "Sounds", - "settings.sounds.likeReceived": "Group invitation received", - "settings.sounds.groupNewMember": "Group invitation accepted", - "settings.sounds.matchStarted": "SendouQ match started", - "settings.sounds.tournamentMatchStarted": "Tournament match started", - "settings.mapPool.notOk": "Pick {{count}} stages per mode that you didn't avoid to save your preferences", - "settings.misc.header": "Misc", - "settings.banned": "Banned", - "settings.avoid.label": "Avoid {{special}}", "looking.joiningGroupError": "Before joining another group, leave the current one", "looking.goToSettingsPrompt": "To help with group finding, set your weapon pool and voice chat status on the settings page", "looking.inactiveGroup.soon": "Group will be marked inactive. Still looking?", diff --git a/locales/en/settings.json b/locales/en/settings.json new file mode 100644 index 000000000..9b28e0a1a --- /dev/null +++ b/locales/en/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "Preferences", + "tabs.matchProfile": "Match profile", + "tabs.locale": "Locale", + "tabs.theme": "Theme", + "tabs.sounds": "Sounds", + "matchProfile.maps.header": "Stages and modes", + "matchProfile.maps.personal": "Personal", + "matchProfile.maps.preferencesFor": "Preferences for", + "matchProfile.maps.teamExplanation": "Team map list is used for all members when queueing as a team", + "matchProfile.maps.avoid": "Avoid", + "matchProfile.maps.prefer": "Prefer", + "matchProfile.maps.neutral": "Neutral", + "matchProfile.weaponPool.header": "Weapon pool", + "matchProfile.weaponPool.full": "Weapon pool is full", + "matchProfile.voiceChat.header": "Voice chat", + "matchProfile.voiceChat.canVC.header": "Can voice chat?", + "matchProfile.voiceChat.canVC.yes": "Yes", + "matchProfile.voiceChat.canVC.no": "No", + "matchProfile.voiceChat.canVC.listenOnly": "Listen only", + "matchProfile.voiceChat.languages.header": "Your languages", + "matchProfile.voiceChat.languages.placeholder": "Select all that apply", + "matchProfile.mapPool.notOk": "Pick {{count}} stages per mode that you didn't avoid to save your preferences", + "sounds.likeReceived": "Group invitation received", + "sounds.groupNewMember": "Group invitation accepted", + "sounds.matchStarted": "SendouQ match started", + "sounds.tournamentMatchStarted": "Tournament match started" +} diff --git a/locales/es-ES/q.json b/locales/es-ES/q.json index 25e4b178c..2af656c6a 100644 --- a/locales/es-ES/q.json +++ b/locales/es-ES/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Temporada {{nth}} está abierta", "front.preview": "Ver avance de grupos en fila sin unirte", "front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink", - "settings.maps.header": "Mapas y modos", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Evitar", - "settings.maps.prefer": "Preferir", - "settings.maps.neutral": "Neutral", - "settings.weaponPool.header": "Grupo de armas", - "settings.weaponPool.full": "Grupo de armas está lleno", - "settings.voiceChat.header": "Chat de voz", - "settings.voiceChat.canVC.header": "¿Se puede usar chat de voz?", - "settings.voiceChat.canVC.yes": "Sí", - "settings.voiceChat.canVC.no": "No", - "settings.voiceChat.canVC.listenOnly": "Solo escuchar", - "settings.voiceChat.languages.header": "Tus idiomas", - "settings.voiceChat.languages.placeholder": "Elegir los que correspondan", - "settings.sounds.header": "Sonidos", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "Partido de torneo comenzado", - "settings.mapPool.notOk": "Elige {{count}} mapas por modo que no evitaste para guardar tus preferencias", - "settings.misc.header": "Misc", - "settings.banned": "Prohibidos", - "settings.avoid.label": "Evitar {{special}}", "looking.joiningGroupError": "Antes de unirte a otro grupo, deja el grupo actual", "looking.goToSettingsPrompt": "Para encontrar grupos más fácil, elige tu grupo de armas y estado de chat en la página de preferencias", "looking.inactiveGroup.soon": "Grupo será marcado como inactivo. ¿Aún estás buscando?", diff --git a/locales/es-ES/settings.json b/locales/es-ES/settings.json new file mode 100644 index 000000000..3cd15389f --- /dev/null +++ b/locales/es-ES/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Sonidos", + "matchProfile.maps.header": "Mapas y modos", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Evitar", + "matchProfile.maps.prefer": "Preferir", + "matchProfile.maps.neutral": "Neutral", + "matchProfile.weaponPool.header": "Grupo de armas", + "matchProfile.weaponPool.full": "Grupo de armas está lleno", + "matchProfile.voiceChat.header": "Chat de voz", + "matchProfile.voiceChat.canVC.header": "¿Se puede usar chat de voz?", + "matchProfile.voiceChat.canVC.yes": "Sí", + "matchProfile.voiceChat.canVC.no": "No", + "matchProfile.voiceChat.canVC.listenOnly": "Solo escuchar", + "matchProfile.voiceChat.languages.header": "Tus idiomas", + "matchProfile.voiceChat.languages.placeholder": "Elegir los que correspondan", + "matchProfile.mapPool.notOk": "Elige {{count}} mapas por modo que no evitaste para guardar tus preferencias", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "Partido de torneo comenzado" +} diff --git a/locales/es-US/q.json b/locales/es-US/q.json index d7a98cfd9..a6caa2036 100644 --- a/locales/es-US/q.json +++ b/locales/es-US/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Temporada {{nth}} está abierta", "front.preview": "Ver avance de grupos en fila sin unirte", "front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink", - "settings.maps.header": "Escenarios y Estilos", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Evitar", - "settings.maps.prefer": "Preferir", - "settings.maps.neutral": "Neutral", - "settings.weaponPool.header": "Grupo de armas", - "settings.weaponPool.full": "Grupo de armas está lleno", - "settings.voiceChat.header": "Chat de voz", - "settings.voiceChat.canVC.header": "¿Se puede usar chat de voz?", - "settings.voiceChat.canVC.yes": "Sí", - "settings.voiceChat.canVC.no": "No", - "settings.voiceChat.canVC.listenOnly": "Solo escuchar", - "settings.voiceChat.languages.header": "Tus idiomas", - "settings.voiceChat.languages.placeholder": "Elegir los que correspondan", - "settings.sounds.header": "Sonidos", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "Elige {{count}} escenarios por estilo que no evitaste para guardar tus preferencias", - "settings.misc.header": "Misc", - "settings.banned": "Prohibidos", - "settings.avoid.label": "Evitar {{special}}", "looking.joiningGroupError": "Antes de unirte a otro grupo, deja el corriente", "looking.goToSettingsPrompt": "Para encontrar grupos más facil, elige tu grupo de armas y estado de chat en la página de preferencias", "looking.inactiveGroup.soon": "Grupo será marcado como inactivo. ¿Aún estás buscando?", diff --git a/locales/es-US/settings.json b/locales/es-US/settings.json new file mode 100644 index 000000000..503f7c12c --- /dev/null +++ b/locales/es-US/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Sonidos", + "matchProfile.maps.header": "Escenarios y Estilos", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Evitar", + "matchProfile.maps.prefer": "Preferir", + "matchProfile.maps.neutral": "Neutral", + "matchProfile.weaponPool.header": "Grupo de armas", + "matchProfile.weaponPool.full": "Grupo de armas está lleno", + "matchProfile.voiceChat.header": "Chat de voz", + "matchProfile.voiceChat.canVC.header": "¿Se puede usar chat de voz?", + "matchProfile.voiceChat.canVC.yes": "Sí", + "matchProfile.voiceChat.canVC.no": "No", + "matchProfile.voiceChat.canVC.listenOnly": "Solo escuchar", + "matchProfile.voiceChat.languages.header": "Tus idiomas", + "matchProfile.voiceChat.languages.placeholder": "Elegir los que correspondan", + "matchProfile.mapPool.notOk": "Elige {{count}} escenarios por estilo que no evitaste para guardar tus preferencias", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/fr-CA/q.json b/locales/fr-CA/q.json index 440d316ba..7fc6026d1 100644 --- a/locales/fr-CA/q.json +++ b/locales/fr-CA/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/fr-CA/settings.json b/locales/fr-CA/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/fr-CA/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/fr-EU/q.json b/locales/fr-EU/q.json index 7dcfdc846..7801c2d80 100644 --- a/locales/fr-EU/q.json +++ b/locales/fr-EU/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "La saison {{nth}} est ouverte", "front.preview": "Regarder qui est dans la queue sans la rejoindre", "front.preview.explanation": "Cette fonctionnalité n'est disponible que pour les utilisateurs de niveau Supporter (ou supérieur) sur le patreon de sendou.ink.", - "settings.maps.header": "Stages et modes", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Détester", - "settings.maps.prefer": "Préférer", - "settings.maps.neutral": "Neutre", - "settings.weaponPool.header": "Arme jouée", - "settings.weaponPool.full": "Les armes jouer sont remplies", - "settings.voiceChat.header": "Chat vocal", - "settings.voiceChat.canVC.header": "Peux-tu utiliser le chat vocal?", - "settings.voiceChat.canVC.yes": "Oui", - "settings.voiceChat.canVC.no": "Non", - "settings.voiceChat.canVC.listenOnly": "Ecoute seulement", - "settings.voiceChat.languages.header": "Vos langues", - "settings.voiceChat.languages.placeholder": "Selectionner", - "settings.sounds.header": "Notifications", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "Sélectionner {{count}} stages par mode que vous n'avez pas évité pour enregistrer vos préférences", - "settings.misc.header": "Divers", - "settings.banned": "Bannis", - "settings.avoid.label": "Sans {{special}}", "looking.joiningGroupError": "Avant de rejoindre un nouveau groupe, quitté celui actuel", "looking.goToSettingsPrompt": "Pour aider votre recherche de groupe, selectionner vos armes et votre statut vocal dans les paramètres", "looking.inactiveGroup.soon": "Le groupe a été marqué comme inactif. Recherchez-vous toujours?", diff --git a/locales/fr-EU/settings.json b/locales/fr-EU/settings.json new file mode 100644 index 000000000..d91b7832a --- /dev/null +++ b/locales/fr-EU/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Notifications", + "matchProfile.maps.header": "Stages et modes", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Détester", + "matchProfile.maps.prefer": "Préférer", + "matchProfile.maps.neutral": "Neutre", + "matchProfile.weaponPool.header": "Arme jouée", + "matchProfile.weaponPool.full": "Les armes jouer sont remplies", + "matchProfile.voiceChat.header": "Chat vocal", + "matchProfile.voiceChat.canVC.header": "Peux-tu utiliser le chat vocal?", + "matchProfile.voiceChat.canVC.yes": "Oui", + "matchProfile.voiceChat.canVC.no": "Non", + "matchProfile.voiceChat.canVC.listenOnly": "Ecoute seulement", + "matchProfile.voiceChat.languages.header": "Vos langues", + "matchProfile.voiceChat.languages.placeholder": "Selectionner", + "matchProfile.mapPool.notOk": "Sélectionner {{count}} stages par mode que vous n'avez pas évité pour enregistrer vos préférences", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/he/q.json b/locales/he/q.json index 3faca72b3..c1cc2b06a 100644 --- a/locales/he/q.json +++ b/locales/he/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/he/settings.json b/locales/he/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/he/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/it/q.json b/locales/it/q.json index 501946cb4..f1e0d45b7 100644 --- a/locales/it/q.json +++ b/locales/it/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Stagione {{nth}} aperta", "front.preview": "Visualizza gruppi nella coda prima di unirti", "front.preview.explanation": "Questa funzione è disponibile solo per iscritti al Patreon di sendou.ink di tier Supporter o più", - "settings.maps.header": "Mappe e modalità", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Evita", - "settings.maps.prefer": "Preferisci", - "settings.maps.neutral": "Neutrale", - "settings.weaponPool.header": "Pool armi", - "settings.weaponPool.full": "La pool armi è al completo", - "settings.voiceChat.header": "Voice chat", - "settings.voiceChat.canVC.header": "Puoi fare voice chat?", - "settings.voiceChat.canVC.yes": "Sì", - "settings.voiceChat.canVC.no": "No", - "settings.voiceChat.canVC.listenOnly": "Ascolta soltanto", - "settings.voiceChat.languages.header": "Le tue lingue", - "settings.voiceChat.languages.placeholder": "Seleziona quelle che preferisci", - "settings.sounds.header": "Suoni", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "Scegli {{count}} mappe per modalità che non hai evitato per salvare le tue preferenze", - "settings.misc.header": "Misc", - "settings.banned": "Bannata", - "settings.avoid.label": "Evita {{special}}", "looking.joiningGroupError": "Prima di unirti a un nuovo gruppo, lascia quello attuale", "looking.goToSettingsPrompt": "Per aiutarti a trovare un gruppo, imposta la tua pool armi e stato voice chat nella pagina delle impostazioni", "looking.inactiveGroup.soon": "Il gruppo verrà segnato come inattivo. Stai ancora cercando?", diff --git a/locales/it/settings.json b/locales/it/settings.json new file mode 100644 index 000000000..5ac4ab1ef --- /dev/null +++ b/locales/it/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Suoni", + "matchProfile.maps.header": "Mappe e modalità", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Evita", + "matchProfile.maps.prefer": "Preferisci", + "matchProfile.maps.neutral": "Neutrale", + "matchProfile.weaponPool.header": "Pool armi", + "matchProfile.weaponPool.full": "La pool armi è al completo", + "matchProfile.voiceChat.header": "Voice chat", + "matchProfile.voiceChat.canVC.header": "Puoi fare voice chat?", + "matchProfile.voiceChat.canVC.yes": "Sì", + "matchProfile.voiceChat.canVC.no": "No", + "matchProfile.voiceChat.canVC.listenOnly": "Ascolta soltanto", + "matchProfile.voiceChat.languages.header": "Le tue lingue", + "matchProfile.voiceChat.languages.placeholder": "Seleziona quelle che preferisci", + "matchProfile.mapPool.notOk": "Scegli {{count}} mappe per modalità che non hai evitato per salvare le tue preferenze", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/ja/q.json b/locales/ja/q.json index ef6fcd2be..eb69ba157 100644 --- a/locales/ja/q.json +++ b/locales/ja/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "シーズン{{nth}}開幕", "front.preview": "ジョインせず列にいるグループを見る", "front.preview.explanation": "この機能はサポーター(あるいはそれ以上)の支援者のみ使えます。", - "settings.maps.header": "ステージ、モード", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "避ける", - "settings.maps.prefer": "好み", - "settings.maps.neutral": "普通", - "settings.weaponPool.header": "武器プール", - "settings.weaponPool.full": "武器プールがいっぱいです", - "settings.voiceChat.header": "ボイチャ", - "settings.voiceChat.canVC.header": "ボイチャできますか?", - "settings.voiceChat.canVC.yes": "はい", - "settings.voiceChat.canVC.no": "いいえ", - "settings.voiceChat.canVC.listenOnly": "聞き専", - "settings.voiceChat.languages.header": "自分の言語", - "settings.voiceChat.languages.placeholder": "該当するものを全て選んでください。", - "settings.sounds.header": "音", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "モードにつきステージを {{count}} 個選んでください。(避けるステージに入っていない)", - "settings.misc.header": "他", - "settings.banned": "禁止", - "settings.avoid.label": "避ける {{special}}", "looking.joiningGroupError": "違うグループをジョインする前に今入っているやつを出てください", "looking.goToSettingsPrompt": "グループ探しを助けるため武器プール、ボイチャ状態を設定画面でセットしてください", "looking.inactiveGroup.soon": "グループが非活動とマークされます。まだ探していますか?", diff --git a/locales/ja/settings.json b/locales/ja/settings.json new file mode 100644 index 000000000..2150e7e8f --- /dev/null +++ b/locales/ja/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "音", + "matchProfile.maps.header": "ステージ、モード", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "避ける", + "matchProfile.maps.prefer": "好み", + "matchProfile.maps.neutral": "普通", + "matchProfile.weaponPool.header": "武器プール", + "matchProfile.weaponPool.full": "武器プールがいっぱいです", + "matchProfile.voiceChat.header": "ボイチャ", + "matchProfile.voiceChat.canVC.header": "ボイチャできますか?", + "matchProfile.voiceChat.canVC.yes": "はい", + "matchProfile.voiceChat.canVC.no": "いいえ", + "matchProfile.voiceChat.canVC.listenOnly": "聞き専", + "matchProfile.voiceChat.languages.header": "自分の言語", + "matchProfile.voiceChat.languages.placeholder": "該当するものを全て選んでください。", + "matchProfile.mapPool.notOk": "モードにつきステージを {{count}} 個選んでください。(避けるステージに入っていない)", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/ko/q.json b/locales/ko/q.json index 75631dd93..c80c6b27b 100644 --- a/locales/ko/q.json +++ b/locales/ko/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/ko/settings.json b/locales/ko/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/ko/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/nl/q.json b/locales/nl/q.json index 75631dd93..c80c6b27b 100644 --- a/locales/nl/q.json +++ b/locales/nl/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/nl/settings.json b/locales/nl/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/nl/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/pl/q.json b/locales/pl/q.json index 75631dd93..c80c6b27b 100644 --- a/locales/pl/q.json +++ b/locales/pl/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "", "front.preview": "", "front.preview.explanation": "", - "settings.maps.header": "", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "", - "settings.maps.prefer": "", - "settings.maps.neutral": "", - "settings.weaponPool.header": "", - "settings.weaponPool.full": "", - "settings.voiceChat.header": "", - "settings.voiceChat.canVC.header": "", - "settings.voiceChat.canVC.yes": "", - "settings.voiceChat.canVC.no": "", - "settings.voiceChat.canVC.listenOnly": "", - "settings.voiceChat.languages.header": "", - "settings.voiceChat.languages.placeholder": "", - "settings.sounds.header": "", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "", - "settings.misc.header": "", - "settings.banned": "", - "settings.avoid.label": "", "looking.joiningGroupError": "", "looking.goToSettingsPrompt": "", "looking.inactiveGroup.soon": "", diff --git a/locales/pl/settings.json b/locales/pl/settings.json new file mode 100644 index 000000000..6f53df715 --- /dev/null +++ b/locales/pl/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "", + "matchProfile.maps.header": "", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "", + "matchProfile.maps.prefer": "", + "matchProfile.maps.neutral": "", + "matchProfile.weaponPool.header": "", + "matchProfile.weaponPool.full": "", + "matchProfile.voiceChat.header": "", + "matchProfile.voiceChat.canVC.header": "", + "matchProfile.voiceChat.canVC.yes": "", + "matchProfile.voiceChat.canVC.no": "", + "matchProfile.voiceChat.canVC.listenOnly": "", + "matchProfile.voiceChat.languages.header": "", + "matchProfile.voiceChat.languages.placeholder": "", + "matchProfile.mapPool.notOk": "", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/pt-BR/q.json b/locales/pt-BR/q.json index cbea4df20..a0622e681 100644 --- a/locales/pt-BR/q.json +++ b/locales/pt-BR/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Temporada {{nth}} aberta", "front.preview": "Pré-visualizar grupos na fila sem entrar", "front.preview.explanation": "Essa função está disponível apenas para patronos (ou patronesses) da tier Supporter (ou acima) do sendou.ink", - "settings.maps.header": "Mapas e modos", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Evitar", - "settings.maps.prefer": "Prefere", - "settings.maps.neutral": "Neutro", - "settings.weaponPool.header": "Pool de armas", - "settings.weaponPool.full": "A pool de armas está cheia", - "settings.voiceChat.header": "Chat de voz", - "settings.voiceChat.canVC.header": "Pode usar chat de voz?", - "settings.voiceChat.canVC.yes": "Sim", - "settings.voiceChat.canVC.no": "Não", - "settings.voiceChat.canVC.listenOnly": "Posso apenas ouvir", - "settings.voiceChat.languages.header": "Seus idiomas", - "settings.voiceChat.languages.placeholder": "Escolha todos que se aplicam", - "settings.sounds.header": "Sons", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "Escolha {{count}} mapas por modo que você não evitou para salvar suas preferências", - "settings.misc.header": "Diversos", - "settings.banned": "Banido(a)", - "settings.avoid.label": "Evitar {{special}}", "looking.joiningGroupError": "Antes de entrar em outro grupo, saia do atual", "looking.goToSettingsPrompt": "Para ajudar a encontrar um grupo defina sua pool de armas e status de chat de voz na página de configurações", "looking.inactiveGroup.soon": "O grupo será marcado como inativo. Ainda procurando?", diff --git a/locales/pt-BR/settings.json b/locales/pt-BR/settings.json new file mode 100644 index 000000000..b099c361a --- /dev/null +++ b/locales/pt-BR/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Sons", + "matchProfile.maps.header": "Mapas e modos", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Evitar", + "matchProfile.maps.prefer": "Prefere", + "matchProfile.maps.neutral": "Neutro", + "matchProfile.weaponPool.header": "Pool de armas", + "matchProfile.weaponPool.full": "A pool de armas está cheia", + "matchProfile.voiceChat.header": "Chat de voz", + "matchProfile.voiceChat.canVC.header": "Pode usar chat de voz?", + "matchProfile.voiceChat.canVC.yes": "Sim", + "matchProfile.voiceChat.canVC.no": "Não", + "matchProfile.voiceChat.canVC.listenOnly": "Posso apenas ouvir", + "matchProfile.voiceChat.languages.header": "Seus idiomas", + "matchProfile.voiceChat.languages.placeholder": "Escolha todos que se aplicam", + "matchProfile.mapPool.notOk": "Escolha {{count}} mapas por modo que você não evitou para salvar suas preferências", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/ru/q.json b/locales/ru/q.json index 7bfc27c49..2564769fe 100644 --- a/locales/ru/q.json +++ b/locales/ru/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "Сезон {{nth}} открыт", "front.preview": "Предпросмотр груп в очереди без присоединения", "front.preview.explanation": "Данная функция доступна только меценатам sendou.imk уровня Supporter (или выше)", - "settings.maps.header": "Арены и режимы", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "Избегаю", - "settings.maps.prefer": "Предпочитаю", - "settings.maps.neutral": "Нейтрально", - "settings.weaponPool.header": "Пул оружия", - "settings.weaponPool.full": "Достигнут максимум", - "settings.voiceChat.header": "Голосовой чат", - "settings.voiceChat.canVC.header": "Можете ли вы присоединиться к голосовому чату?", - "settings.voiceChat.canVC.yes": "Да", - "settings.voiceChat.canVC.no": "Нет", - "settings.voiceChat.canVC.listenOnly": "Только слушать", - "settings.voiceChat.languages.header": "Ваши языки", - "settings.voiceChat.languages.placeholder": "Выбрать языки", - "settings.sounds.header": "Звуковые уведомления", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "Выберите по {{count}} арен за каждый режим, который вы не избегаете, чтобы сохранить настройки", - "settings.misc.header": "Прочее", - "settings.banned": "Запрещённый режим", - "settings.avoid.label": "Избегать {{special}}", "looking.joiningGroupError": "Перед тем как присоединиться к новой группе, покиньте текущую", "looking.goToSettingsPrompt": "Чтобы облегчить процесс нахождения группы, настройте ваш пул оружия и статус голосового чата на странице настроек", "looking.inactiveGroup.soon": "Группа будет помечена, как неактивная. Ещё ищете?", diff --git a/locales/ru/settings.json b/locales/ru/settings.json new file mode 100644 index 000000000..b276da98a --- /dev/null +++ b/locales/ru/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "Звуковые уведомления", + "matchProfile.maps.header": "Арены и режимы", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "Избегаю", + "matchProfile.maps.prefer": "Предпочитаю", + "matchProfile.maps.neutral": "Нейтрально", + "matchProfile.weaponPool.header": "Пул оружия", + "matchProfile.weaponPool.full": "Достигнут максимум", + "matchProfile.voiceChat.header": "Голосовой чат", + "matchProfile.voiceChat.canVC.header": "Можете ли вы присоединиться к голосовому чату?", + "matchProfile.voiceChat.canVC.yes": "Да", + "matchProfile.voiceChat.canVC.no": "Нет", + "matchProfile.voiceChat.canVC.listenOnly": "Только слушать", + "matchProfile.voiceChat.languages.header": "Ваши языки", + "matchProfile.voiceChat.languages.placeholder": "Выбрать языки", + "matchProfile.mapPool.notOk": "Выберите по {{count}} арен за каждый режим, который вы не избегаете, чтобы сохранить настройки", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/locales/zh/q.json b/locales/zh/q.json index 5e66fff87..521058963 100644 --- a/locales/zh/q.json +++ b/locales/zh/q.json @@ -50,31 +50,6 @@ "front.seasonOpen": "赛季 {{nth}} 进行中", "front.preview": "预览当前匹配中的队伍", "front.preview.explanation": "该功能仅对sendou.ink的patron支持者开放", - "settings.maps.header": "地图和模式", - "settings.maps.personal": "", - "settings.maps.preferencesFor": "", - "settings.maps.teamExplanation": "", - "settings.maps.avoid": "避开", - "settings.maps.prefer": "偏好", - "settings.maps.neutral": "中立", - "settings.weaponPool.header": "武器池", - "settings.weaponPool.full": "武器池已满", - "settings.voiceChat.header": "语音", - "settings.voiceChat.canVC.header": "您可以语音吗?", - "settings.voiceChat.canVC.yes": "是", - "settings.voiceChat.canVC.no": "否", - "settings.voiceChat.canVC.listenOnly": "仅能收听", - "settings.voiceChat.languages.header": "您的语言", - "settings.voiceChat.languages.placeholder": "选择所有符合的选项", - "settings.sounds.header": "提示音", - "settings.sounds.likeReceived": "", - "settings.sounds.groupNewMember": "", - "settings.sounds.matchStarted": "", - "settings.sounds.tournamentMatchStarted": "", - "settings.mapPool.notOk": "每个模式选择 {{count}} 个您不想避开的地图以设置地图偏好", - "settings.misc.header": "其他", - "settings.banned": "禁止", - "settings.avoid.label": "禁止 {{special}}", "looking.joiningGroupError": "您需要离开当前小队才能加入其他小队", "looking.goToSettingsPrompt": "请在设置页面设置您的武器池以及语音状态", "looking.inactiveGroup.soon": "小队将被标记为不活跃。要继续匹配吗?", diff --git a/locales/zh/settings.json b/locales/zh/settings.json new file mode 100644 index 000000000..d0d1c7539 --- /dev/null +++ b/locales/zh/settings.json @@ -0,0 +1,28 @@ +{ + "tabs.preferences": "", + "tabs.matchProfile": "", + "tabs.locale": "", + "tabs.theme": "", + "tabs.sounds": "提示音", + "matchProfile.maps.header": "地图和模式", + "matchProfile.maps.personal": "", + "matchProfile.maps.preferencesFor": "", + "matchProfile.maps.teamExplanation": "", + "matchProfile.maps.avoid": "避开", + "matchProfile.maps.prefer": "偏好", + "matchProfile.maps.neutral": "中立", + "matchProfile.weaponPool.header": "武器池", + "matchProfile.weaponPool.full": "武器池已满", + "matchProfile.voiceChat.header": "语音", + "matchProfile.voiceChat.canVC.header": "您可以语音吗?", + "matchProfile.voiceChat.canVC.yes": "是", + "matchProfile.voiceChat.canVC.no": "否", + "matchProfile.voiceChat.canVC.listenOnly": "仅能收听", + "matchProfile.voiceChat.languages.header": "您的语言", + "matchProfile.voiceChat.languages.placeholder": "选择所有符合的选项", + "matchProfile.mapPool.notOk": "每个模式选择 {{count}} 个您不想避开的地图以设置地图偏好", + "sounds.likeReceived": "", + "sounds.groupNewMember": "", + "sounds.matchStarted": "", + "sounds.tournamentMatchStarted": "" +} diff --git a/migrations/143-match-profile-rename.js b/migrations/143-match-profile-rename.js new file mode 100644 index 000000000..c7b41d150 --- /dev/null +++ b/migrations/143-match-profile-rename.js @@ -0,0 +1,5 @@ +export function up(db) { + db.prepare( + /* sql */ `alter table "User" rename column "qWeaponPool" to "weaponPool"`, + ).run(); +} diff --git a/scripts/map-popularity-q.ts b/scripts/map-popularity-q.ts index d72cf8a46..52e7153d7 100644 --- a/scripts/map-popularity-q.ts +++ b/scripts/map-popularity-q.ts @@ -1,6 +1,6 @@ import "dotenv/config"; import { db } from "~/db/sql"; -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; diff --git a/scripts/map-popularity.ts b/scripts/map-popularity.ts index bb7c4de6b..dfcea8757 100644 --- a/scripts/map-popularity.ts +++ b/scripts/map-popularity.ts @@ -1,6 +1,6 @@ import "dotenv/config"; import { db } from "~/db/sql"; -import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; diff --git a/scripts/transfer-weapon-pools.ts b/scripts/transfer-weapon-pools.ts index 9bc68f5ca..79eaf0c2f 100644 --- a/scripts/transfer-weapon-pools.ts +++ b/scripts/transfer-weapon-pools.ts @@ -36,7 +36,7 @@ async function main() { await db .updateTable("User") .set({ - qWeaponPool: JSON.stringify(weaponPoolIds), + weaponPool: JSON.stringify(weaponPoolIds), }) .where("User.id", "=", Number(userId)) .execute();