mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-14 14:22:38 -05:00
Match page get rid of join tab
This commit is contained in:
@@ -2,12 +2,8 @@ import clsx from "clsx";
|
||||
import { ArrowLeft, MessageSquare, X } from "lucide-react";
|
||||
import { Button } from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useFetcher } from "react-router";
|
||||
import { Link } from "react-router";
|
||||
import { useCurrentRouteChatCode } from "~/features/chat/ChatProvider";
|
||||
import {
|
||||
extractRoomLink,
|
||||
isMatchRoomUrl,
|
||||
} from "~/features/chat/chat-constants";
|
||||
import { resolveDatePlaceholders } from "~/features/chat/chat-utils";
|
||||
import { Chat } from "~/features/chat/components/Chat";
|
||||
import { useChatContext } from "~/features/chat/useChatContext";
|
||||
@@ -178,7 +174,6 @@ function ChatView({ onClose }: { onClose?: () => void }) {
|
||||
.filter(([code]) => code !== activeRoom)
|
||||
.reduce((sum, [, count]) => sum + count, 0);
|
||||
|
||||
const roomLinkFetcher = useFetcher();
|
||||
const room = chatContext.rooms.find((r) => r.chatCode === activeRoom);
|
||||
const roomExpired = Boolean(room?.expiresAt && room.expiresAt < Date.now());
|
||||
const messages = chatContext.messagesForRoom(activeRoom);
|
||||
@@ -194,26 +189,10 @@ function ChatView({ onClose }: { onClose?: () => void }) {
|
||||
}
|
||||
}
|
||||
|
||||
const isMatchRoom = room?.url ? isMatchRoomUrl(room.url) : false;
|
||||
|
||||
const chatAdapter = {
|
||||
messages,
|
||||
send: (contents: string) => {
|
||||
chatContext.send(activeRoom, contents);
|
||||
|
||||
if (isMatchRoom) {
|
||||
const link = extractRoomLink(contents);
|
||||
if (link) {
|
||||
roomLinkFetcher.submit(
|
||||
{ _action: "UPSERT", url: link },
|
||||
{
|
||||
method: "post",
|
||||
action: "/room",
|
||||
encType: "application/json",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
currentRoom: activeRoom,
|
||||
setCurrentRoom: () => {},
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
width: 100%;
|
||||
height: var(--banner-height);
|
||||
border-radius: var(--radius-box);
|
||||
padding: var(--s-2);
|
||||
padding: var(--s-2-5);
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.6)
|
||||
),
|
||||
var(--stage-img);
|
||||
@@ -50,32 +53,39 @@
|
||||
bottom: var(--s-2);
|
||||
right: var(--s-2);
|
||||
display: flex;
|
||||
gap: var(--s-0-5);
|
||||
gap: var(--s-1);
|
||||
align-items: center;
|
||||
color: var(--color-text-high);
|
||||
background-color: var(--color-bg-high);
|
||||
padding: var(--s-0-5) var(--s-1-5);
|
||||
border-radius: var(--radius-field);
|
||||
font-size: var(--font-3xs);
|
||||
font-weight: normal;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.joinBadge {
|
||||
.joinInfo {
|
||||
position: absolute;
|
||||
bottom: var(--s-2);
|
||||
left: var(--s-2);
|
||||
display: flex;
|
||||
gap: var(--s-1);
|
||||
align-items: center;
|
||||
color: var(--color-text-high);
|
||||
background-color: var(--color-bg-high);
|
||||
padding: var(--s-0-5) var(--s-1-5);
|
||||
border-radius: var(--radius-field);
|
||||
font-size: var(--font-2xs);
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.joinInfoItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.joinInfoLabel {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-xs);
|
||||
font-weight: var(--weight-semi);
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.joinInfoValue {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--weight-bold);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.infoBadge {
|
||||
@@ -120,8 +130,11 @@
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.6),
|
||||
rgba(0, 0, 0, 0.6)
|
||||
),
|
||||
var(--stage-img);
|
||||
@@ -149,7 +162,8 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--s-1);
|
||||
gap: var(--s-0-5);
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
height: var(--banner-height);
|
||||
border-radius: var(--radius-box);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { Check, QrCode, X } from "lucide-react";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
@@ -22,7 +21,7 @@ interface MatchBannerProps {
|
||||
mode: ModeShort;
|
||||
screenLegal?: boolean;
|
||||
joinPool?: string | null;
|
||||
joinViaQr?: boolean;
|
||||
joinPass?: string | null;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,7 @@ export function MatchBanner({
|
||||
mode,
|
||||
screenLegal,
|
||||
joinPool,
|
||||
joinViaQr,
|
||||
joinPass,
|
||||
children,
|
||||
}: MatchBannerProps) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
@@ -50,7 +49,7 @@ export function MatchBanner({
|
||||
</div>
|
||||
<div className={clsx(styles.info, styles.thickText)}>{children}</div>
|
||||
|
||||
{joinPool ? <JoinPoolBadge pool={joinPool} viaQr={joinViaQr} /> : null}
|
||||
{joinPool ? <JoinInfo pool={joinPool} pass={joinPass} /> : null}
|
||||
{screenLegal !== undefined ? (
|
||||
<ScreenNotice screenLegal={screenLegal} />
|
||||
) : null}
|
||||
@@ -82,7 +81,7 @@ interface IconBannerProps {
|
||||
subtitle?: string;
|
||||
screenLegal?: boolean;
|
||||
joinPool?: string | null;
|
||||
joinViaQr?: boolean;
|
||||
joinPass?: string | null;
|
||||
topRight?: React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
@@ -93,7 +92,7 @@ export function IconBanner({
|
||||
subtitle,
|
||||
screenLegal,
|
||||
joinPool,
|
||||
joinViaQr,
|
||||
joinPass,
|
||||
topRight,
|
||||
testId,
|
||||
}: IconBannerProps) {
|
||||
@@ -104,7 +103,7 @@ export function IconBanner({
|
||||
{subtitle ? (
|
||||
<div className={styles.iconBannerSubtitle}>{subtitle}</div>
|
||||
) : null}
|
||||
{joinPool ? <JoinPoolBadge pool={joinPool} viaQr={joinViaQr} /> : null}
|
||||
{joinPool ? <JoinInfo pool={joinPool} pass={joinPass} /> : null}
|
||||
{screenLegal !== undefined ? (
|
||||
<ScreenNotice screenLegal={screenLegal} />
|
||||
) : null}
|
||||
@@ -115,35 +114,33 @@ export function IconBanner({
|
||||
);
|
||||
}
|
||||
|
||||
function JoinPoolBadge({ pool, viaQr }: { pool: string; viaQr?: boolean }) {
|
||||
function JoinInfo({ pool, pass }: { pool: string; pass?: string | null }) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
className={styles.joinBadge}
|
||||
onPress={() =>
|
||||
setSearchParams(
|
||||
{ tab: "join" },
|
||||
{
|
||||
preventScrollReset: true,
|
||||
defaultShouldRevalidate: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
aria-label={t("q:match.pool")}
|
||||
testId="join-pool-badge"
|
||||
>
|
||||
{viaQr ? <QrCode size={18} /> : pool}
|
||||
</SendouButton>
|
||||
<div className={styles.joinInfo}>
|
||||
<div className={styles.joinInfoItem}>
|
||||
<div className={styles.joinInfoLabel}>{t("q:match.pool")}</div>
|
||||
<div className={styles.joinInfoValue}>{pool}</div>
|
||||
</div>
|
||||
{pass ? (
|
||||
<div className={styles.joinInfoItem}>
|
||||
<div className={styles.joinInfoLabel}>
|
||||
{t("q:match.password.short")}
|
||||
</div>
|
||||
<div className={styles.joinInfoValue} data-testid="room-pass">
|
||||
{pass}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScreenNotice({ screenLegal }: { screenLegal: boolean }) {
|
||||
const { t } = useTranslation(["weapons", "q"]);
|
||||
|
||||
const imgSize = 18;
|
||||
const imgSize = 24;
|
||||
|
||||
const Icon = screenLegal ? Check : X;
|
||||
|
||||
@@ -156,16 +153,16 @@ function ScreenNotice({ screenLegal }: { screenLegal: boolean }) {
|
||||
testId={screenLegal ? "screen-allowed" : "screen-banned"}
|
||||
aria-label={screenLegal ? "Screen allowed" : "Screen banned"}
|
||||
>
|
||||
<Icon
|
||||
size={imgSize}
|
||||
className={screenLegal ? styles.legalIcon : styles.illegalIcon}
|
||||
/>
|
||||
<img
|
||||
src={`${specialWeaponImageUrl(19)}.avif`}
|
||||
width={imgSize}
|
||||
height={imgSize}
|
||||
alt=""
|
||||
/>
|
||||
<Icon
|
||||
size={imgSize}
|
||||
className={screenLegal ? styles.legalIcon : styles.illegalIcon}
|
||||
/>
|
||||
</SendouButton>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
.joinContent {
|
||||
display: grid;
|
||||
grid-template-areas: "time x" "qr join";
|
||||
grid-template-columns: auto minmax(0, max-content);
|
||||
gap: var(--s-1) var(--s-4);
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.joinInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-area: join;
|
||||
gap: var(--s-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--font-2xs);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--weight-semi);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.infoValueTruncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.qrCodeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--s-1);
|
||||
grid-area: qr;
|
||||
}
|
||||
|
||||
.roomAge {
|
||||
grid-area: time;
|
||||
font-size: var(--font-2xs);
|
||||
color: var(--color-text-high);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrCode {
|
||||
background-color: white;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.joinLink {
|
||||
font-size: var(--font-2xs);
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-wrap: nowrap;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.qrOverlay {
|
||||
width: 172px;
|
||||
height: 172px;
|
||||
border-radius: var(--radius-field);
|
||||
padding: var(--s-2);
|
||||
background-color: var(--color-bg-higher);
|
||||
grid-area: qr;
|
||||
}
|
||||
|
||||
.stalePrompt {
|
||||
display: flex;
|
||||
gap: var(--s-6);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.staleText {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-text-high);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.noRoomHint {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-text-high);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { useFormatDistanceToNow } from "~/hooks/intl/useFormatDistanceToNow";
|
||||
import { SendouButton } from "../elements/Button";
|
||||
import { SendouTabPanel } from "../elements/Tabs";
|
||||
import styles from "./MatchJoinTab.module.css";
|
||||
import { TAB_KEYS } from "./MatchTabs";
|
||||
|
||||
interface MatchJoinTabProps {
|
||||
joinLink?: string;
|
||||
hostedBy?: string;
|
||||
pool: string;
|
||||
pass: string;
|
||||
showNoSplatnetAlert: boolean;
|
||||
isStale?: boolean;
|
||||
staleMinutesAgo?: number;
|
||||
refreshedAt?: Date;
|
||||
onConfirmRoom?: () => void;
|
||||
isConfirming?: boolean;
|
||||
}
|
||||
|
||||
export function MatchJoinTab({
|
||||
joinLink,
|
||||
hostedBy,
|
||||
pool,
|
||||
pass,
|
||||
showNoSplatnetAlert,
|
||||
isStale,
|
||||
staleMinutesAgo,
|
||||
refreshedAt,
|
||||
onConfirmRoom,
|
||||
isConfirming,
|
||||
}: MatchJoinTabProps) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const formatDistanceToNow = useFormatDistanceToNow();
|
||||
|
||||
return (
|
||||
<SendouTabPanel id={TAB_KEYS.JOIN}>
|
||||
<div className="stack lg">
|
||||
{showNoSplatnetAlert ? (
|
||||
<Alert variation="WARNING" tiny>
|
||||
{t("q:match.noSplatnetWarning")}
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className={styles.joinContent}>
|
||||
{joinLink ? (
|
||||
isStale ? (
|
||||
<StaleRoomPrompt
|
||||
minutesAgo={staleMinutesAgo ?? 0}
|
||||
onConfirm={onConfirmRoom}
|
||||
isConfirming={isConfirming}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{refreshedAt ? (
|
||||
<div className={styles.roomAge}>
|
||||
{formatDistanceToNow(refreshedAt, { addSuffix: true })}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.qrCodeContainer}>
|
||||
<QRCodeSVG
|
||||
value={joinLink}
|
||||
size={140}
|
||||
className={styles.qrCode}
|
||||
/>
|
||||
<a
|
||||
href={joinLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.joinLink}
|
||||
>
|
||||
{joinLink}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<div className={clsx(styles.qrOverlay, styles.noRoomHint)}>
|
||||
{t("q:match.room.noRoomHint")}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.joinInfo}>
|
||||
{hostedBy ? (
|
||||
<InfoWithHeader
|
||||
header={t("q:match.hostedBy")}
|
||||
value={hostedBy}
|
||||
truncate
|
||||
/>
|
||||
) : null}
|
||||
<InfoWithHeader header={t("q:match.pool")} value={pool} />
|
||||
<InfoWithHeader
|
||||
header={t("q:match.password.short")}
|
||||
value={pass}
|
||||
testId="room-pass"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SendouTabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function StaleRoomPrompt({
|
||||
minutesAgo,
|
||||
onConfirm,
|
||||
isConfirming,
|
||||
}: {
|
||||
minutesAgo: number;
|
||||
onConfirm?: () => void;
|
||||
isConfirming?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.qrOverlay, styles.stalePrompt)}>
|
||||
<div className={styles.staleText}>
|
||||
{t("q:match.room.stalePrompt", { minutes: minutesAgo })}
|
||||
</div>
|
||||
<SendouButton
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onPress={onConfirm}
|
||||
isDisabled={isConfirming}
|
||||
>
|
||||
{t("q:match.room.confirm")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoWithHeader({
|
||||
header,
|
||||
value,
|
||||
testId,
|
||||
truncate,
|
||||
}: {
|
||||
header: string;
|
||||
value: string;
|
||||
testId?: string;
|
||||
truncate?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.infoHeader}>{header}</div>
|
||||
<div
|
||||
className={clsx(styles.infoValue, {
|
||||
[styles.infoValueTruncate]: truncate,
|
||||
})}
|
||||
data-testid={testId}
|
||||
title={truncate ? value : undefined}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +50,8 @@ interface RosterTabTeam {
|
||||
subbedOut?: Array<number>;
|
||||
tier?: { name: TierName; isPlus: boolean };
|
||||
seed?: number | null;
|
||||
/** Whether this team is expected to host the room (tournament only). */
|
||||
isHost?: boolean;
|
||||
}
|
||||
|
||||
interface MatchRosterTabProps {
|
||||
@@ -261,6 +263,8 @@ function TeamHeader({
|
||||
label: string;
|
||||
dotClassName: string;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
const tierText = team.tier
|
||||
? `${team.tier.name.toLowerCase()}${team.tier.isPlus ? "+" : ""}`
|
||||
: undefined;
|
||||
@@ -282,6 +286,12 @@ function TeamHeader({
|
||||
<span>{seedText}</span>
|
||||
</>
|
||||
) : null}
|
||||
{team.isHost ? (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{t("common:host")}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import {
|
||||
BarChart3,
|
||||
DoorOpen,
|
||||
Key,
|
||||
ScrollText,
|
||||
Tally5,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { BarChart3, Key, ScrollText, Tally5, Users } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { SendouTab, SendouTabList, SendouTabs } from "../elements/Tabs";
|
||||
import styles from "./MatchTabs.module.css";
|
||||
@@ -27,7 +19,6 @@ const TAB_KEY = "tab";
|
||||
export const TAB_KEYS = {
|
||||
ROSTERS: "rosters",
|
||||
ACTION: "action",
|
||||
JOIN: "join",
|
||||
RESULT: "result",
|
||||
STATS: "stats",
|
||||
ADMIN: "admin",
|
||||
@@ -36,7 +27,6 @@ export const TAB_KEYS = {
|
||||
const TAB_ICONS: Record<MatchTabsKey, React.ReactNode> = {
|
||||
rosters: <Users />,
|
||||
action: <Tally5 />,
|
||||
join: <DoorOpen />,
|
||||
result: <ScrollText />,
|
||||
stats: <BarChart3 />,
|
||||
admin: <Key />,
|
||||
@@ -45,7 +35,6 @@ const TAB_ICONS: Record<MatchTabsKey, React.ReactNode> = {
|
||||
const TAB_TRANSLATION_KEYS = {
|
||||
rosters: "q:match.tabs.rosters",
|
||||
action: "q:match.tabs.action",
|
||||
join: "common:actions.join",
|
||||
result: "q:match.tabs.result",
|
||||
stats: "q:match.tabs.stats",
|
||||
admin: "common:pages.admin",
|
||||
@@ -54,12 +43,9 @@ const TAB_TRANSLATION_KEYS = {
|
||||
export function MatchTabs({ children, tabs, alertTabs }: MatchTabsProps) {
|
||||
const { t } = useTranslation(["q", "common"]);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const user = useUser();
|
||||
|
||||
const preferredTab = user?.preferences.defaultMatchPageTab;
|
||||
const currentTab =
|
||||
tabs.find((tab) => searchParams.get(TAB_KEY) === tab) ??
|
||||
(preferredTab && tabs.includes(preferredTab) ? preferredTab : tabs.at(0));
|
||||
tabs.find((tab) => searchParams.get(TAB_KEY) === tab) ?? tabs.at(0);
|
||||
invariant(currentTab);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2726,23 +2726,6 @@ async function groups(variation?: SeedVariation | null) {
|
||||
expiresAfter: { hours: 2 },
|
||||
});
|
||||
}
|
||||
|
||||
const thirtyMinutesAgo = dateToDatabaseTimestamp(
|
||||
sub(new Date(), { minutes: 30 }),
|
||||
);
|
||||
sql
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
insert into "RoomLink" ("userId", "url", "createdAt", "refreshedAt")
|
||||
values (@userId, @url, @createdAt, @refreshedAt)
|
||||
`,
|
||||
)
|
||||
.run({
|
||||
userId: ADMIN_ID,
|
||||
url: "https://example.com//private_battle/seed_room_123",
|
||||
createdAt: thirtyMinutesAgo,
|
||||
refreshedAt: thirtyMinutesAgo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1070,8 +1070,6 @@ export interface UserPreferences {
|
||||
/** Is spoiler-free mode enabled? Hides recent tournament results and scores until the user chooses to reveal them. */
|
||||
spoilerFreeMode?: boolean;
|
||||
weaponReportDefaultOpen?: boolean;
|
||||
/** Which tab opens first on the match page. Unset = first available tab (Rosters). */
|
||||
defaultMatchPageTab?: "rosters" | "join" | "action";
|
||||
}
|
||||
|
||||
export const SUBJECT_PRONOUNS = ["he", "she", "they", "it", "any"] as const;
|
||||
@@ -1133,8 +1131,6 @@ export interface User {
|
||||
weaponPool: JSONColumnTypeNullable<WeaponPoolEntry[]>;
|
||||
plusSkippedForSeasonNth: number | null;
|
||||
noScreen: Generated<DBBoolean>;
|
||||
/** User doesn't have access to SplatNet 3 to join rooms made by others */
|
||||
noSplatnet: Generated<DBBoolean>;
|
||||
buildSorting: JSONColumnTypeNullable<BuildSort[]>;
|
||||
preferences: JSONColumnTypeNullable<UserPreferences>;
|
||||
/** User creation date. Can be null because we did not always save this. */
|
||||
@@ -1435,13 +1431,6 @@ export interface NotificationUserSubscription {
|
||||
subscription: JSONColumnType<NotificationSubscription>;
|
||||
}
|
||||
|
||||
export interface RoomLink {
|
||||
userId: number;
|
||||
url: string;
|
||||
createdAt: Generated<number>;
|
||||
refreshedAt: Generated<number>;
|
||||
}
|
||||
|
||||
export const SPLATOON_ROTATION_TYPES = ["SERIES", "OPEN", "X"] as const;
|
||||
export type SplatoonRotationType = (typeof SPLATOON_ROTATION_TYPES)[number];
|
||||
|
||||
@@ -1500,7 +1489,6 @@ export interface DB {
|
||||
PlusTier: PlusTier;
|
||||
PlusVote: PlusVote;
|
||||
PlusVotingResult: PlusVotingResult;
|
||||
RoomLink: RoomLink;
|
||||
ReportedWeapon: ReportedWeapon;
|
||||
Skill: Skill;
|
||||
SkillTeamUser: SkillTeamUser;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { sub } from "date-fns";
|
||||
import { db } from "~/db/sql";
|
||||
import { actorId } from "~/features/auth/core/user.server";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
|
||||
export function upsertOwn(url: string) {
|
||||
return db
|
||||
.insertInto("RoomLink")
|
||||
.values({
|
||||
userId: actorId(),
|
||||
url,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.column("userId").doUpdateSet({
|
||||
url,
|
||||
createdAt: databaseTimestampNow(),
|
||||
refreshedAt: databaseTimestampNow(),
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function findByUserIds(userIds: number[], maxAgeHours: number) {
|
||||
return db
|
||||
.selectFrom("RoomLink")
|
||||
.select([
|
||||
"RoomLink.userId",
|
||||
"RoomLink.url",
|
||||
"RoomLink.createdAt",
|
||||
"RoomLink.refreshedAt",
|
||||
])
|
||||
.where("RoomLink.userId", "in", userIds)
|
||||
.where(
|
||||
"RoomLink.createdAt",
|
||||
">=",
|
||||
dateToDatabaseTimestamp(sub(new Date(), { hours: maxAgeHours })),
|
||||
)
|
||||
.orderBy("RoomLink.refreshedAt", "asc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function refreshOwnTimestamp() {
|
||||
return db
|
||||
.updateTable("RoomLink")
|
||||
.set({ refreshedAt: databaseTimestampNow() })
|
||||
.where("userId", "=", actorId())
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function deleteOld() {
|
||||
return db
|
||||
.deleteFrom("RoomLink")
|
||||
.where(
|
||||
"refreshedAt",
|
||||
"<",
|
||||
dateToDatabaseTimestamp(sub(new Date(), { hours: 2 })),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
extractRoomLink,
|
||||
findRoomLinks,
|
||||
isSplatnetRoomUrl,
|
||||
} from "./chat-constants";
|
||||
import { findRoomLinks, isSplatnetRoomUrl } from "./chat-constants";
|
||||
|
||||
describe("isSplatnetRoomUrl", () => {
|
||||
test("accepts canonical SplatNet share path", () => {
|
||||
@@ -111,15 +107,3 @@ describe("findRoomLinks", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractRoomLink", () => {
|
||||
test("returns first valid link", () => {
|
||||
expect(extractRoomLink("hi https://s.nintendo.com/abc see you")).toBe(
|
||||
"https://s.nintendo.com/abc",
|
||||
);
|
||||
});
|
||||
|
||||
test("returns null when no valid link present", () => {
|
||||
expect(extractRoomLink("https://sanintendoacom.evil.tld/abc")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,18 +31,6 @@ export function findRoomLinks(
|
||||
return results;
|
||||
}
|
||||
|
||||
export function extractRoomLink(text: string): string | null {
|
||||
return findRoomLinks(text)[0]?.url ?? null;
|
||||
}
|
||||
|
||||
const MATCH_ROOM_URL_PATTERN =
|
||||
/^\/q\/match\/\d+$|^\/to\/\d+\/matches\/\d+$|^\/scrims\/\d+$/;
|
||||
|
||||
export function isMatchRoomUrl(url: string) {
|
||||
const pathname = canParseUrl(url) ? new URL(url).pathname : url;
|
||||
return MATCH_ROOM_URL_PATTERN.test(pathname);
|
||||
}
|
||||
|
||||
function canParseUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
|
||||
@@ -107,6 +107,20 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.roomLinkBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--s-1);
|
||||
margin-block: var(--s-1);
|
||||
}
|
||||
|
||||
.roomQrCode {
|
||||
background-color: white;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.roomLink {
|
||||
color: var(--color-text-accent);
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { sub } from "date-fns";
|
||||
import { SendHorizontal } from "lucide-react";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import * as React from "react";
|
||||
import { Button } from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -325,15 +326,17 @@ function MessageContents({ text }: { text: string }) {
|
||||
parts.push(text.slice(lastIndex, match.index));
|
||||
}
|
||||
parts.push(
|
||||
<a
|
||||
key={i}
|
||||
href={match.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.roomLink}
|
||||
>
|
||||
{match.url}
|
||||
</a>,
|
||||
<span key={i} className={styles.roomLinkBlock}>
|
||||
<QRCodeSVG value={match.url} size={120} className={styles.roomQrCode} />
|
||||
<a
|
||||
href={match.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.roomLink}
|
||||
>
|
||||
{match.url}
|
||||
</a>
|
||||
</span>,
|
||||
);
|
||||
lastIndex = match.index + match.url.length;
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import { useFetcher } from "react-router";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
|
||||
interface RoomLink {
|
||||
userId: number;
|
||||
url: string;
|
||||
refreshedAt: number;
|
||||
}
|
||||
|
||||
interface ResolveActiveRoomLinkArgs {
|
||||
/** Room links for all match participants, sorted by `refreshedAt` ascending. */
|
||||
roomLinks: ReadonlyArray<RoomLink>;
|
||||
/** Database timestamp before which a link is considered stale (e.g. match start time). */
|
||||
freshnessCutoff: number;
|
||||
/** Viewer user id, used as fallback to surface the viewer's own stale link. */
|
||||
viewerUserId?: number;
|
||||
/** Members shown to resolve `hostedBy`. */
|
||||
members: ReadonlyArray<{ id: number; username: string }>;
|
||||
}
|
||||
|
||||
interface ActiveRoomLink {
|
||||
joinLink?: string;
|
||||
hostedBy?: string;
|
||||
isStale?: boolean;
|
||||
staleMinutesAgo: number;
|
||||
refreshedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the room link to display for a match. Prefers the oldest link refreshed
|
||||
* after the freshness cutoff (the host's confirmed room). Falls back to the
|
||||
* viewer's own stale link so they can refresh it themselves.
|
||||
*/
|
||||
export function resolveActiveRoomLink({
|
||||
roomLinks,
|
||||
freshnessCutoff,
|
||||
viewerUserId,
|
||||
members,
|
||||
}: ResolveActiveRoomLinkArgs): ActiveRoomLink {
|
||||
const validRoomLink = roomLinks.find(
|
||||
(rl) => rl.refreshedAt >= freshnessCutoff,
|
||||
);
|
||||
const ownStaleRoomLink = validRoomLink
|
||||
? undefined
|
||||
: roomLinks.find((rl) => rl.userId === viewerUserId);
|
||||
|
||||
const activeRoomLink = validRoomLink ?? ownStaleRoomLink;
|
||||
|
||||
return {
|
||||
joinLink: activeRoomLink?.url,
|
||||
hostedBy: activeRoomLink
|
||||
? members.find((m) => m.id === activeRoomLink.userId)?.username
|
||||
: undefined,
|
||||
isStale: activeRoomLink ? !validRoomLink : undefined,
|
||||
staleMinutesAgo: ownStaleRoomLink
|
||||
? differenceInMinutes(
|
||||
new Date(),
|
||||
databaseTimestampToDate(ownStaleRoomLink.refreshedAt),
|
||||
)
|
||||
: 0,
|
||||
refreshedAt: validRoomLink
|
||||
? databaseTimestampToDate(validRoomLink.refreshedAt)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Confirms the viewer's room link by refreshing its timestamp via the central `/room` action. */
|
||||
export function useConfirmRoom() {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return {
|
||||
onConfirmRoom: () => {
|
||||
fetcher.submit(
|
||||
{ _action: "CONFIRM" },
|
||||
{ method: "post", action: "/room", encType: "application/json" },
|
||||
);
|
||||
},
|
||||
isConfirming: fetcher.state !== "idle",
|
||||
};
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { ActionFunctionArgs } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { parseRequestPayload } from "~/utils/remix.server";
|
||||
import { isSplatnetRoomUrl } from "../chat-constants";
|
||||
import * as RoomLinkRepository from "../RoomLinkRepository.server";
|
||||
|
||||
const roomLinkSchema = z.discriminatedUnion("_action", [
|
||||
z.object({
|
||||
_action: z.literal("UPSERT"),
|
||||
url: z.string().refine(isSplatnetRoomUrl, "Not a SplatNet room URL"),
|
||||
}),
|
||||
z.object({
|
||||
_action: z.literal("CONFIRM"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const data = await parseRequestPayload({
|
||||
request,
|
||||
schema: roomLinkSchema,
|
||||
});
|
||||
|
||||
switch (data._action) {
|
||||
case "UPSERT": {
|
||||
await RoomLinkRepository.upsertOwn(data.url);
|
||||
break;
|
||||
}
|
||||
case "CONFIRM": {
|
||||
await RoomLinkRepository.refreshOwnTimestamp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { MatchBannerBottomRow } from "~/components/match-page/MatchBannerBottomRow";
|
||||
import { MatchBannerTimer } from "~/components/match-page/MatchBannerTimer";
|
||||
import { MatchBannerTopRow } from "~/components/match-page/MatchBannerTopRow";
|
||||
import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
|
||||
import { MatchPage } from "~/components/match-page/MatchPage";
|
||||
import { MatchPageHeader } from "~/components/match-page/MatchPageHeader";
|
||||
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
|
||||
@@ -96,6 +95,8 @@ export default function MatchPageTestRoute() {
|
||||
teamName: "Chimera",
|
||||
})}
|
||||
screenLegal={false}
|
||||
joinPool="SQ7"
|
||||
joinPass="8430"
|
||||
/>
|
||||
<MatchBannerBottomRow
|
||||
games={[{ mode: "SZ" }, { mode: "TC" }, { mode: "RM" }]}
|
||||
@@ -172,13 +173,7 @@ export default function MatchPageTestRoute() {
|
||||
/>
|
||||
</MatchBannerContainer>
|
||||
|
||||
<MatchTabs tabs={["join", "rosters", "action", "result"]}>
|
||||
<MatchJoinTab
|
||||
joinLink="https://app.nintendo.net/private_battle/abc123"
|
||||
pool="SQ7"
|
||||
pass="8430"
|
||||
showNoSplatnetAlert
|
||||
/>
|
||||
<MatchTabs tabs={["rosters", "action", "result"]}>
|
||||
<MatchRosterTab
|
||||
minMembersPerTeam={4}
|
||||
canEditSubbedOut={[true, false]}
|
||||
|
||||
@@ -14,7 +14,6 @@ export async function settingsByUserId(userId: number) {
|
||||
"User.vc",
|
||||
"User.languages",
|
||||
"User.noScreen",
|
||||
"User.noSplatnet",
|
||||
matchProfileWeapons(eb).as("weaponPool"),
|
||||
])
|
||||
.where("id", "=", userId)
|
||||
@@ -49,14 +48,12 @@ export async function updateOwnMatchProfile({
|
||||
languages,
|
||||
weaponPool,
|
||||
noScreen,
|
||||
noSplatnet,
|
||||
}: {
|
||||
mapModePreferences: UserMapModePreferences;
|
||||
vc: Tables["User"]["vc"];
|
||||
languages: string[];
|
||||
weaponPool: WeaponPoolItem[];
|
||||
noScreen: number;
|
||||
noSplatnet: number;
|
||||
}) {
|
||||
const userId = actorId();
|
||||
const currentPreferences = (
|
||||
@@ -102,7 +99,6 @@ export async function updateOwnMatchProfile({
|
||||
vc,
|
||||
languages: languages.length > 0 ? languages.join(",") : null,
|
||||
noScreen,
|
||||
noSplatnet,
|
||||
})
|
||||
.where("id", "=", userId)
|
||||
.execute();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { sub } from "date-fns";
|
||||
import { Ban, Swords } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData } from "react-router";
|
||||
@@ -9,20 +8,14 @@ import {
|
||||
} from "~/components/match-page/MatchBanner";
|
||||
import { MatchBannerScheduledTime } from "~/components/match-page/MatchBannerScheduledTime";
|
||||
import { MatchBannerTopRow } from "~/components/match-page/MatchBannerTopRow";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { resolveActiveRoomLink } from "~/features/chat/room-link-utils";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
dateToDatabaseTimestamp,
|
||||
} from "~/utils/dates";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import * as Scrim from "../core/Scrim";
|
||||
import type { loader } from "../loaders/scrims.$id.server";
|
||||
import { SCRIM } from "../scrims-constants";
|
||||
|
||||
export function ScrimMatchBanner() {
|
||||
const { t } = useTranslation(["scrims"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
|
||||
const screenLegal = !data.anyUserPrefersNoScreen;
|
||||
|
||||
@@ -45,17 +38,8 @@ export function ScrimMatchBanner() {
|
||||
);
|
||||
}
|
||||
|
||||
const acceptedRequest = data.post.requests[0];
|
||||
const activeRoomLink = resolveActiveRoomLink({
|
||||
roomLinks: data.roomLinks,
|
||||
freshnessCutoff: dateToDatabaseTimestamp(
|
||||
sub(new Date(), { minutes: SCRIM.ROOM_LINK_FRESHNESS_MINUTES }),
|
||||
),
|
||||
viewerUserId: user?.id,
|
||||
members: [...data.post.users, ...acceptedRequest.users],
|
||||
});
|
||||
const joinViaQr = Boolean(activeRoomLink.joinLink) && !activeRoomLink.isStale;
|
||||
const joinPool = Scrim.resolvePoolCode(data.post.id);
|
||||
const joinPass = resolveRoomPass(data.post.id);
|
||||
|
||||
const currentMap = data.mapByMap.currentMap;
|
||||
|
||||
@@ -68,7 +52,7 @@ export function ScrimMatchBanner() {
|
||||
mode={currentMap.mode}
|
||||
screenLegal={screenLegal}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
/>
|
||||
</MatchBannerContainer>
|
||||
);
|
||||
@@ -83,7 +67,7 @@ export function ScrimMatchBanner() {
|
||||
subtitle={t("scrims:banner.freeForm.subtitle")}
|
||||
screenLegal={screenLegal}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
/>
|
||||
</MatchBannerContainer>
|
||||
);
|
||||
|
||||
@@ -1,59 +1,27 @@
|
||||
import { sub } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData } from "react-router";
|
||||
import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
|
||||
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
|
||||
import { MatchRosterTab } from "~/components/match-page/MatchRosterTab";
|
||||
import { MatchTabs, TAB_KEYS } from "~/components/match-page/MatchTabs";
|
||||
import type { TimelineMap } from "~/components/match-page/MatchTimeline";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import {
|
||||
resolveActiveRoomLink,
|
||||
useConfirmRoom,
|
||||
} from "~/features/chat/room-link-utils";
|
||||
import {
|
||||
databaseTimestampToJavascriptTimestamp,
|
||||
dateToDatabaseTimestamp,
|
||||
} from "~/utils/dates";
|
||||
import { databaseTimestampToJavascriptTimestamp } from "~/utils/dates";
|
||||
import { teamPage } from "~/utils/urls";
|
||||
import * as Scrim from "../core/Scrim";
|
||||
import type { loader } from "../loaders/scrims.$id.server";
|
||||
import { SCRIM } from "../scrims-constants";
|
||||
import type { ScrimPost } from "../scrims-types";
|
||||
import { ScrimMatchActionTab } from "./ScrimMatchActionTab";
|
||||
import { ScrimMatchStatsTab } from "./ScrimMatchStatsTab";
|
||||
|
||||
export function ScrimMatchTabs() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { onConfirmRoom, isConfirming } = useConfirmRoom();
|
||||
|
||||
const acceptedRequest = data.post.requests[0];
|
||||
const allMembers = [...data.post.users, ...acceptedRequest.users];
|
||||
|
||||
const activeRoomLink = resolveActiveRoomLink({
|
||||
roomLinks: data.roomLinks,
|
||||
freshnessCutoff: dateToDatabaseTimestamp(
|
||||
sub(new Date(), { minutes: SCRIM.ROOM_LINK_FRESHNESS_MINUTES }),
|
||||
),
|
||||
viewerUserId: user?.id,
|
||||
members: allMembers,
|
||||
});
|
||||
|
||||
const tabs = resolveTabs(data);
|
||||
|
||||
return (
|
||||
<MatchTabs tabs={tabs}>
|
||||
<MatchJoinTab
|
||||
{...activeRoomLink}
|
||||
onConfirmRoom={onConfirmRoom}
|
||||
isConfirming={isConfirming}
|
||||
pool={Scrim.resolvePoolCode(data.post.id)}
|
||||
pass={resolveRoomPass(data.post.id)}
|
||||
showNoSplatnetAlert={data.anyUserPrefersNoSplatnet}
|
||||
/>
|
||||
<MatchRosterTab
|
||||
minMembersPerTeam={4}
|
||||
teams={[
|
||||
@@ -96,7 +64,6 @@ export function ScrimMatchTabs() {
|
||||
function resolveTabs(data: ReturnType<typeof useLoaderData<typeof loader>>) {
|
||||
const tabs: Array<(typeof TAB_KEYS)[keyof typeof TAB_KEYS]> = [
|
||||
TAB_KEYS.ROSTERS,
|
||||
TAB_KEYS.JOIN,
|
||||
];
|
||||
|
||||
if (!data.mapByMap?.locked) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { chatAccessible } from "~/features/chat/chat-utils";
|
||||
import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { notFoundIfFalsy } from "../../../utils/remix.server";
|
||||
@@ -31,12 +30,8 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
|
||||
const participantIds = Scrim.participantIdsListFromAccepted(post);
|
||||
|
||||
const [anyUserPrefersNoScreen, anyUserPrefersNoSplatnet, roomLinks] =
|
||||
await Promise.all([
|
||||
UserRepository.anyUserPrefersNoScreen(participantIds),
|
||||
UserRepository.anyUserPrefersNoSplatnet(participantIds),
|
||||
RoomLinkRepository.findByUserIds(participantIds, 3),
|
||||
]);
|
||||
const anyUserPrefersNoScreen =
|
||||
await UserRepository.anyUserPrefersNoScreen(participantIds);
|
||||
|
||||
const mapByMap = await resolveMapByMap({ post, user });
|
||||
|
||||
@@ -52,8 +47,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
? post.chatCode
|
||||
: undefined,
|
||||
anyUserPrefersNoScreen,
|
||||
anyUserPrefersNoSplatnet,
|
||||
roomLinks,
|
||||
mapByMap,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ export const SCRIM = {
|
||||
CANCEL_REASON_MAX_LENGTH: 500,
|
||||
REQUEST_MESSAGE_MAX_LENGTH: 200,
|
||||
MAX_TIME_RANGE_MS: 3 * 60 * 60 * 1000, // 3 hours
|
||||
ROOM_LINK_FRESHNESS_MINUTES: 30,
|
||||
AUTO_CANCEL_WINDOW_HOURS: 1,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import { MatchBannerBottomRow } from "~/components/match-page/MatchBannerBottomR
|
||||
import { MatchBannerStartedAt } from "~/components/match-page/MatchBannerStartedAt";
|
||||
import { MatchBannerTimer } from "~/components/match-page/MatchBannerTimer";
|
||||
import { MatchBannerTopRow } from "~/components/match-page/MatchBannerTopRow";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { resolveActiveRoomLink } from "~/features/chat/room-link-utils";
|
||||
import { SENDOUQ_BEST_OF } from "~/features/sendouq/q-constants";
|
||||
import { useAutoRerender } from "~/hooks/useAutoRerender";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
@@ -90,16 +90,7 @@ export function SendouQMatchBanner({ data }: { data: SendouQMatchLoaderData }) {
|
||||
);
|
||||
|
||||
const joinPool = isParticipant ? `SQ${String(data.match.id).at(-1)}` : null;
|
||||
const activeRoomLink = resolveActiveRoomLink({
|
||||
roomLinks: data.roomLinks,
|
||||
freshnessCutoff: data.match.createdAt,
|
||||
viewerUserId: user?.id,
|
||||
members: [
|
||||
...data.match.groupAlpha.members,
|
||||
...data.match.groupBravo.members,
|
||||
],
|
||||
});
|
||||
const joinViaQr = Boolean(activeRoomLink.joinLink) && !activeRoomLink.isStale;
|
||||
const joinPass = isParticipant ? resolveRoomPass(data.match.id) : null;
|
||||
|
||||
return (
|
||||
<MatchBannerContainer>
|
||||
@@ -112,7 +103,7 @@ export function SendouQMatchBanner({ data }: { data: SendouQMatchLoaderData }) {
|
||||
teamName: cancelRequesterName,
|
||||
})}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
/>
|
||||
) : (
|
||||
<MatchBanner
|
||||
@@ -122,7 +113,7 @@ export function SendouQMatchBanner({ data }: { data: SendouQMatchLoaderData }) {
|
||||
!data.match.groupAlpha.noScreen && !data.match.groupBravo.noScreen
|
||||
}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
>
|
||||
<CurrentMapVotesBadge voters={currentMap.voters} />
|
||||
</MatchBanner>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
|
||||
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
|
||||
import { MatchRosterTab } from "~/components/match-page/MatchRosterTab";
|
||||
import { MatchTabs } from "~/components/match-page/MatchTabs";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import {
|
||||
resolveActiveRoomLink,
|
||||
useConfirmRoom,
|
||||
} from "~/features/chat/room-link-utils";
|
||||
import { ACTION_TAB_AFTER_LOCKED_SECONDS } from "~/features/sendouq/q-constants";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
@@ -28,7 +22,6 @@ import { SendouQMatchActionTab } from "./SendouQMatchActionTab";
|
||||
export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
|
||||
const user = useUser();
|
||||
const isStaff = useHasRole("STAFF");
|
||||
const { onConfirmRoom, isConfirming } = useConfirmRoom();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(["q"]);
|
||||
@@ -90,8 +83,7 @@ export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
|
||||
reporterSide !== null &&
|
||||
reporterSide !== userSide;
|
||||
|
||||
const tabs: Array<"join" | "rosters" | "action" | "result"> = ["rosters"];
|
||||
if (!isLocked && isParticipant) tabs.push("join");
|
||||
const tabs: Array<"rosters" | "action" | "result"> = ["rosters"];
|
||||
if (showActionTab) tabs.push("action");
|
||||
if (isLocked || hasReportedMaps) tabs.push("result");
|
||||
|
||||
@@ -99,18 +91,6 @@ export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
|
||||
? ["action"]
|
||||
: undefined;
|
||||
|
||||
const allMembers = [
|
||||
...data.match.groupAlpha.members,
|
||||
...data.match.groupBravo.members,
|
||||
];
|
||||
|
||||
const activeRoomLink = resolveActiveRoomLink({
|
||||
roomLinks: data.roomLinks,
|
||||
freshnessCutoff: data.match.createdAt,
|
||||
viewerUserId: user?.id,
|
||||
members: allMembers,
|
||||
});
|
||||
|
||||
const ownGroup =
|
||||
userSide === "ALPHA"
|
||||
? data.match.groupAlpha
|
||||
@@ -146,16 +126,6 @@ export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
|
||||
) : null}
|
||||
</MatchResultTab>
|
||||
) : null}
|
||||
{!isLocked && isParticipant ? (
|
||||
<MatchJoinTab
|
||||
{...activeRoomLink}
|
||||
onConfirmRoom={onConfirmRoom}
|
||||
isConfirming={isConfirming}
|
||||
pool={`SQ${String(data.match.id).at(-1)}`}
|
||||
pass={resolveRoomPass(data.match.id)}
|
||||
showNoSplatnetAlert={data.anyUserPrefersNoSplatnet}
|
||||
/>
|
||||
) : null}
|
||||
<MatchRosterTab
|
||||
minMembersPerTeam={4}
|
||||
canEditSubbedOut={[false, false]}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import { chatAccessible } from "~/features/chat/chat-utils";
|
||||
import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
|
||||
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
|
||||
@@ -31,22 +29,16 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
|
||||
const isStaff = user?.roles.includes("STAFF") ?? false;
|
||||
const isParticipant = Boolean(user && matchUsers.includes(user.id));
|
||||
const canSeeRoomLinks = isStaff || isParticipant;
|
||||
|
||||
const [privateNotes, roomLinks, anyUserPrefersNoSplatnet, reportedWeapons] =
|
||||
await Promise.all([
|
||||
user ? PrivateUserNoteRepository.ownNotes(matchUsers) : undefined,
|
||||
canSeeRoomLinks ? RoomLinkRepository.findByUserIds(matchUsers, 3) : [],
|
||||
UserRepository.anyUserPrefersNoSplatnet(matchUsers),
|
||||
ReportedWeaponRepository.findByMatchId(matchId),
|
||||
]);
|
||||
const [privateNotes, reportedWeapons] = await Promise.all([
|
||||
user ? PrivateUserNoteRepository.ownNotes(matchUsers) : undefined,
|
||||
ReportedWeaponRepository.findByMatchId(matchId),
|
||||
]);
|
||||
|
||||
const match = SendouQ.mapMatch(matchUnmapped, user, privateNotes);
|
||||
|
||||
return {
|
||||
match,
|
||||
roomLinks,
|
||||
anyUserPrefersNoSplatnet,
|
||||
reportedWeapons,
|
||||
isOffSeason: Seasons.current() === null,
|
||||
chatCode: (() => {
|
||||
|
||||
@@ -58,12 +58,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UPDATE_DEFAULT_MATCH_PAGE_TAB": {
|
||||
await UserRepository.updateOwnPreferences({
|
||||
defaultMatchPageTab: data.newValue,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UPDATE_MATCH_PROFILE": {
|
||||
await MatchProfileRepository.updateOwnMatchProfile({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
@@ -71,7 +65,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
languages: data.languages,
|
||||
weaponPool: data.weaponPool,
|
||||
noScreen: Number(data.noScreen),
|
||||
noSplatnet: Number(data.noSplatnet),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export function MatchProfileTab() {
|
||||
vc: matchProfile.vc ?? "NO",
|
||||
languages: matchProfile.languages ?? [],
|
||||
noScreen: Boolean(matchProfile.noScreen),
|
||||
noSplatnet: Boolean(matchProfile.noSplatnet),
|
||||
}}
|
||||
revalidateRoot
|
||||
>
|
||||
@@ -50,7 +49,6 @@ export function MatchProfileTab() {
|
||||
<FormField name="weaponPool" />
|
||||
<FormField name="vc" />
|
||||
<FormField name="languages" />
|
||||
<FormField name="noSplatnet" />
|
||||
<FormField name="noScreen" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Config } from "~/config";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import {
|
||||
defaultMatchPageTabSchema,
|
||||
disableBuildAbilitySortingSchema,
|
||||
disallowScrimPickupsFromUntrustedSchema,
|
||||
spoilerFreeModeSchema,
|
||||
@@ -58,18 +57,6 @@ export function PreferencesTab() {
|
||||
>
|
||||
{({ FormField }) => <FormField name="newValue" />}
|
||||
</SendouForm>
|
||||
<SendouForm
|
||||
schema={defaultMatchPageTabSchema}
|
||||
defaultValues={{
|
||||
newValue: user.preferences.defaultMatchPageTab ?? "rosters",
|
||||
}}
|
||||
autoSubmit
|
||||
revalidateRoot
|
||||
fullWidth
|
||||
hideRequiredIndicator
|
||||
>
|
||||
{({ FormField }) => <FormField name="newValue" />}
|
||||
</SendouForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -65,10 +65,6 @@ export const updateMatchProfileSchema = z.object({
|
||||
label: "labels.languages",
|
||||
items: LANGUAGE_OPTIONS,
|
||||
}),
|
||||
noSplatnet: toggle({
|
||||
label: "labels.noSplatnet",
|
||||
bottomText: "bottomTexts.noScreen",
|
||||
}),
|
||||
noScreen: toggle({
|
||||
label: "labels.noScreen",
|
||||
bottomText: "bottomTexts.noScreen",
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
customField,
|
||||
radioGroup,
|
||||
select,
|
||||
stringConstant,
|
||||
toggle,
|
||||
} from "~/form/fields";
|
||||
import { customField, select, stringConstant, toggle } from "~/form/fields";
|
||||
import { themeInputSchema } from "~/utils/zod";
|
||||
|
||||
const customThemeSchema = z.object({
|
||||
@@ -54,18 +48,6 @@ const weaponReportDefaultOpenSchema = z.object({
|
||||
newValue: z.boolean(),
|
||||
});
|
||||
|
||||
export const defaultMatchPageTabSchema = z.object({
|
||||
_action: stringConstant("UPDATE_DEFAULT_MATCH_PAGE_TAB"),
|
||||
newValue: radioGroup({
|
||||
label: "labels.defaultMatchPageTab",
|
||||
items: [
|
||||
{ value: "rosters", label: "options.defaultMatchPageTab.rosters" },
|
||||
{ value: "join", label: "options.defaultMatchPageTab.join" },
|
||||
{ value: "action", label: "options.defaultMatchPageTab.action" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
export const settingsEditSchema = z.union([
|
||||
customThemeSchema,
|
||||
disableBuildAbilitySortingSchema,
|
||||
@@ -73,5 +55,4 @@ export const settingsEditSchema = z.union([
|
||||
spoilerFreeModeSchema,
|
||||
clockFormatSchema,
|
||||
weaponReportDefaultOpenSchema,
|
||||
defaultMatchPageTabSchema,
|
||||
]);
|
||||
|
||||
@@ -55,10 +55,8 @@ export function TournamentMatchBanner({
|
||||
teamsMissingActiveRoster,
|
||||
matchIsLocked,
|
||||
joinPool,
|
||||
activeRoomLink,
|
||||
joinPass,
|
||||
} = useMatch();
|
||||
const joinViaQr =
|
||||
Boolean(activeRoomLink?.joinLink) && !activeRoomLink?.isStale;
|
||||
|
||||
const opponentOne = data.match.opponentOne;
|
||||
const opponentTwo = data.match.opponentTwo;
|
||||
@@ -152,7 +150,7 @@ export function TournamentMatchBanner({
|
||||
})}
|
||||
screenLegal={screenLegal}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
testId="active-roster-needed-text"
|
||||
/>
|
||||
) : pickBanBanner ? (
|
||||
@@ -162,7 +160,7 @@ export function TournamentMatchBanner({
|
||||
subtitle={pickBanBanner.subtitle}
|
||||
screenLegal={screenLegal}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
/>
|
||||
) : currentMap ? (
|
||||
<MatchBanner
|
||||
@@ -170,7 +168,7 @@ export function TournamentMatchBanner({
|
||||
mode={currentMap.mode}
|
||||
screenLegal={screenLegal}
|
||||
joinPool={joinPool}
|
||||
joinViaQr={joinViaQr}
|
||||
joinPass={joinPass}
|
||||
>
|
||||
<CurrentMapPickInfo
|
||||
source={currentMap.source}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetcher } from "react-router";
|
||||
import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
|
||||
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
|
||||
import { MatchRosterTab } from "~/components/match-page/MatchRosterTab";
|
||||
import { MatchTabs, TAB_KEYS } from "~/components/match-page/MatchTabs";
|
||||
@@ -8,9 +7,7 @@ import type {
|
||||
TimelineMap,
|
||||
TimelinePickBanEvent,
|
||||
} from "~/components/match-page/MatchTimeline";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useConfirmRoom } from "~/features/chat/room-link-utils";
|
||||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
|
||||
import { tournamentTeamToActiveRosterUserIds } from "~/features/tournament-bracket/tournament-bracket-utils";
|
||||
@@ -88,9 +85,6 @@ export function TournamentMatchTabs({
|
||||
isOngoing={!data.matchIsOver && data.results.length > 0}
|
||||
/>
|
||||
) : null}
|
||||
{tabs.includes(TAB_KEYS.JOIN) ? (
|
||||
<TournamentMatchJoinTab data={data} />
|
||||
) : null}
|
||||
<TournamentMatchRosterTab data={data} />
|
||||
{tabs.includes(TAB_KEYS.ACTION) ? (
|
||||
isPickBanStep && turnOfResult ? (
|
||||
@@ -312,30 +306,6 @@ function slotOfEvent({
|
||||
}
|
||||
}
|
||||
|
||||
function TournamentMatchJoinTab({ data }: { data: TournamentMatchLoaderData }) {
|
||||
const { onConfirmRoom, isConfirming } = useConfirmRoom();
|
||||
const {
|
||||
teams: [teamOne, teamTwo],
|
||||
joinPool,
|
||||
activeRoomLink,
|
||||
} = useMatch();
|
||||
if (!teamOne || !teamTwo || !joinPool || !activeRoomLink) return null;
|
||||
|
||||
const hostingTeam = resolveHostingTeam([teamOne, teamTwo]);
|
||||
|
||||
return (
|
||||
<MatchJoinTab
|
||||
{...activeRoomLink}
|
||||
hostedBy={activeRoomLink.hostedBy ?? hostingTeam.name}
|
||||
onConfirmRoom={onConfirmRoom}
|
||||
isConfirming={isConfirming}
|
||||
pool={joinPool}
|
||||
pass={resolveRoomPass(hostingTeam.id)}
|
||||
showNoSplatnetAlert={data.anyUserPrefersNoSplatnet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TournamentMatchRosterTab({
|
||||
data,
|
||||
}: {
|
||||
@@ -351,6 +321,9 @@ function TournamentMatchRosterTab({
|
||||
|
||||
const tbdTeam = { defaultName: t("tournament:match.tbd"), members: [] };
|
||||
|
||||
const hostingTeamId =
|
||||
teamOne && teamTwo ? resolveHostingTeam([teamOne, teamTwo]).id : null;
|
||||
|
||||
return (
|
||||
<MatchRosterTab
|
||||
minMembersPerTeam={tournament.minMembersPerTeam}
|
||||
@@ -404,6 +377,7 @@ function TournamentMatchRosterTab({
|
||||
})),
|
||||
subbedOut,
|
||||
seed: team.seed,
|
||||
isHost: hostingTeamId === team.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { LoaderFunctionArgs } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
|
||||
import { chatAccessible } from "~/features/chat/chat-utils";
|
||||
import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
||||
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
|
||||
@@ -218,16 +217,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
(isParticipant || tournament.isOrganizerOrStreamer(user)) &&
|
||||
!isLeagueRoundLocked(tournament, match.roundId);
|
||||
|
||||
const [roomLinks, anyUserPrefersNoSplatnet] = canJoin
|
||||
? await Promise.all([
|
||||
RoomLinkRepository.findByUserIds(
|
||||
match.players.map((p) => p.id),
|
||||
3,
|
||||
),
|
||||
UserRepository.anyUserPrefersNoSplatnet(match.players.map((p) => p.id)),
|
||||
])
|
||||
: ([[], false] as const);
|
||||
|
||||
return {
|
||||
match: hasPermsToSeeChat ? match : { ...match, chatCode: undefined },
|
||||
results,
|
||||
@@ -238,8 +227,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
noScreen,
|
||||
chatCode: visibleChatCode,
|
||||
canJoin,
|
||||
roomLinks,
|
||||
anyUserPrefersNoSplatnet,
|
||||
pickBanEventCount: pickBanEvents.length,
|
||||
pickBanEvents: pickBanEvents.map((e) => ({
|
||||
type: e.type,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { TAB_KEYS } from "~/components/match-page/MatchTabs";
|
||||
import { resolveRoomPass } from "~/components/match-page/utils";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { resolveActiveRoomLink } from "~/features/chat/room-link-utils";
|
||||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import { isLeagueRoundLocked } from "~/features/tournament/tournament-utils";
|
||||
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
import type { TournamentMatchLoaderData } from "./loaders/to.$id.matches.$mid.server";
|
||||
import { matchIsLocked, resolveHostingTeam } from "./tournament-match-utils";
|
||||
|
||||
type ActiveRoomLink = ReturnType<typeof resolveActiveRoomLink>;
|
||||
|
||||
export type MatchPageTeam = NonNullable<ReturnType<Tournament["teamById"]>>;
|
||||
|
||||
export type MatchTabKey = (typeof TAB_KEYS)[keyof typeof TAB_KEYS];
|
||||
@@ -35,7 +33,7 @@ type MatchPageContextValue = {
|
||||
isPickBanStep: boolean;
|
||||
matchIsLocked: boolean;
|
||||
joinPool: string | null;
|
||||
activeRoomLink: ActiveRoomLink | null;
|
||||
joinPass: string | null;
|
||||
};
|
||||
|
||||
const MatchPageContext = React.createContext<MatchPageContextValue | null>(
|
||||
@@ -101,16 +99,7 @@ export function MatchPageProvider({
|
||||
scores,
|
||||
});
|
||||
|
||||
const joinPool = resolveJoinPool({ tournament, data, teams });
|
||||
|
||||
const activeRoomLink = data.canJoin
|
||||
? resolveActiveRoomLink({
|
||||
roomLinks: data.roomLinks,
|
||||
freshnessCutoff: data.match.startedAt ?? 0,
|
||||
viewerUserId: user?.id,
|
||||
members: data.match.players,
|
||||
})
|
||||
: null;
|
||||
const joinInfo = resolveJoinInfo({ tournament, data, teams });
|
||||
|
||||
const tabs = resolveVisibleTabs({
|
||||
canReportScore: tournament.canReportScore({
|
||||
@@ -119,7 +108,6 @@ export function MatchPageProvider({
|
||||
}),
|
||||
canReportWeapons:
|
||||
isParticipant && tournament.weaponReportingOpen && hasReportedMaps,
|
||||
canJoin: data.canJoin,
|
||||
hasCurrentMap: Boolean(currentMap),
|
||||
hasMissingActiveRoster: teamsMissingActiveRoster.length > 0,
|
||||
hasReportedMaps,
|
||||
@@ -144,8 +132,8 @@ export function MatchPageProvider({
|
||||
turnOfResult,
|
||||
isPickBanStep,
|
||||
matchIsLocked: lockedForCast,
|
||||
joinPool,
|
||||
activeRoomLink,
|
||||
joinPool: joinInfo?.pool ?? null,
|
||||
joinPass: joinInfo?.pass ?? null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -164,7 +152,6 @@ export function useMatch() {
|
||||
function resolveVisibleTabs({
|
||||
canReportScore,
|
||||
canReportWeapons,
|
||||
canJoin,
|
||||
hasCurrentMap,
|
||||
hasMissingActiveRoster,
|
||||
hasReportedMaps,
|
||||
@@ -176,7 +163,6 @@ function resolveVisibleTabs({
|
||||
}: {
|
||||
canReportScore: boolean;
|
||||
canReportWeapons: boolean;
|
||||
canJoin: boolean;
|
||||
hasCurrentMap: boolean;
|
||||
hasMissingActiveRoster: boolean;
|
||||
hasReportedMaps: boolean;
|
||||
@@ -188,9 +174,6 @@ function resolveVisibleTabs({
|
||||
}): MatchTabKey[] {
|
||||
const tabs: MatchTabKey[] = [TAB_KEYS.ROSTERS];
|
||||
|
||||
if (canJoin) {
|
||||
tabs.push(TAB_KEYS.JOIN);
|
||||
}
|
||||
if (
|
||||
!leagueRoundLocked &&
|
||||
(isPickBanStep ||
|
||||
@@ -214,7 +197,7 @@ function resolveVisibleTabs({
|
||||
return tabs;
|
||||
}
|
||||
|
||||
function resolveJoinPool({
|
||||
function resolveJoinInfo({
|
||||
tournament,
|
||||
data,
|
||||
teams,
|
||||
@@ -222,7 +205,7 @@ function resolveJoinPool({
|
||||
tournament: ReturnType<typeof useTournament>;
|
||||
data: TournamentMatchLoaderData;
|
||||
teams: [MatchPageTeam | null, MatchPageTeam | null];
|
||||
}): string | null {
|
||||
}): { pool: string; pass: string } | null {
|
||||
if (!data.canJoin) return null;
|
||||
|
||||
const [teamOne, teamTwo] = teams;
|
||||
@@ -254,7 +237,10 @@ function resolveJoinPool({
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return `${poolCode.prefix}${poolCode.suffix}`;
|
||||
return {
|
||||
pool: `${poolCode.prefix}${poolCode.suffix}`,
|
||||
pass: resolveRoomPass(hostingTeam.id),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTeamsMissingActiveRoster(
|
||||
|
||||
@@ -1312,21 +1312,6 @@ export async function anyUserPrefersNoScreen(
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
export async function anyUserPrefersNoSplatnet(
|
||||
userIds: number[],
|
||||
): Promise<boolean> {
|
||||
if (userIds.length === 0) return false;
|
||||
|
||||
const result = await db
|
||||
.selectFrom("User")
|
||||
.select("User.noSplatnet")
|
||||
.where("User.id", "in", userIds)
|
||||
.where("User.noSplatnet", "=", 1)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
export async function socialLinksByUserId(userId: number) {
|
||||
const user = await db
|
||||
.selectFrom("User")
|
||||
|
||||
@@ -272,7 +272,6 @@ export default [
|
||||
route("/admin", "features/admin/routes/admin.tsx"),
|
||||
route("/admin/streams", "features/admin/routes/admin.streams.tsx"),
|
||||
route("/api/chat-users", "features/chat/routes/api.chat-users.ts"),
|
||||
route("/room", "features/chat/routes/room.ts"),
|
||||
route("/api", "features/api/routes/api.tsx"),
|
||||
|
||||
...prefix("/a", [
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as RoomLinkRepository from "../features/chat/RoomLinkRepository.server";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Routine } from "./routine.server";
|
||||
|
||||
export const DeleteOldRoomLinksRoutine = new Routine({
|
||||
name: "DeleteOldRoomLinks",
|
||||
func: async () => {
|
||||
const { numDeletedRows } = await RoomLinkRepository.deleteOld();
|
||||
logger.info(`Deleted ${numDeletedRows} old room links`);
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import { CloseExpiredContinueVotesRoutine } from "./closeExpiredContinueVotes";
|
||||
import { DeleteObsoleteMatchVodsRoutine } from "./deleteObsoleteMatchVods";
|
||||
import { DeleteOldExternalStreamsRoutine } from "./deleteOldExternalStreams";
|
||||
import { DeleteOldNotificationsRoutine } from "./deleteOldNotifications";
|
||||
import { DeleteOldRoomLinksRoutine } from "./deleteOldRoomLinks";
|
||||
import { DeleteOldTournamentAuditLogsRoutine } from "./deleteOldTournamentAuditLogs";
|
||||
import { DeleteOrphanArtTagsRoutine } from "./deleteOrphanArtTags";
|
||||
import { NotifyCheckInStartRoutine } from "./notifyCheckInStart";
|
||||
@@ -32,7 +31,6 @@ export const everyHourAt30 = [
|
||||
SetOldGroupsAsInactiveRoutine,
|
||||
UpdatePatreonDataRoutine,
|
||||
CloseExpiredContinueVotesRoutine,
|
||||
DeleteOldRoomLinksRoutine,
|
||||
DeleteOldExternalStreamsRoutine,
|
||||
];
|
||||
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
@@ -33,12 +33,11 @@ const TAB_LABELS = {
|
||||
admin: "Admin",
|
||||
result: "Result",
|
||||
rosters: "Rosters",
|
||||
join: "Join",
|
||||
} as const;
|
||||
|
||||
export const goToTab = async (
|
||||
page: Page,
|
||||
tab: "action" | "admin" | "result" | "rosters" | "join",
|
||||
tab: "action" | "admin" | "result" | "rosters",
|
||||
) => {
|
||||
// When teams have more members than the minimum, the action tab is hidden
|
||||
// until each team's active roster is locked in via the rosters tab. Auto-set
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,6 @@ import {
|
||||
startBracket,
|
||||
test,
|
||||
} from "./helpers/playwright";
|
||||
import { goToTab } from "./helpers/tournament-match";
|
||||
|
||||
const TOURNAMENT_ID = 2;
|
||||
|
||||
@@ -146,8 +145,6 @@ test.describe("Tournament staff", () => {
|
||||
url: tournamentMatchPage({ tournamentId: TOURNAMENT_ID, matchId: 2 }),
|
||||
});
|
||||
|
||||
await goToTab(page, "join");
|
||||
|
||||
await expect(roomPassSelector).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Bliv medlem",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Note: Hvis du ændrer dit holds navn, så kan andre hold overtage det tidligere holdnavn og URL-adresse.",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kaptajn",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontlinje",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Beitreten",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Hinweis: Wenn du den Namen deines Teams änderst, können andere Teams den Namen und und die URL für sich beanspruchen.",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kapitän",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "Outlined",
|
||||
"actions.noOutline": "No outline",
|
||||
"actions.join": "Join",
|
||||
"host": "Host",
|
||||
"actions.nevermind": "Nevermind",
|
||||
"actions.clickHere": "Click here",
|
||||
"actions.goBack": "Go back",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "Custom role",
|
||||
"labels.teamMemberRoleType": "Role type",
|
||||
"labels.clockFormat": "Clock format",
|
||||
"labels.defaultMatchPageTab": "Default match page tab",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Disable automatic ability sorting",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "Disallow scrim pickups from non-friends",
|
||||
"labels.noScreen": "[Accessibility] Avoid Splattercolor Screen",
|
||||
"labels.noSplatnet": "No SplatNet access",
|
||||
"labels.spoilerFreeMode": "Spoiler-free mode",
|
||||
"bottomTexts.imageModeration": "Unless you're a Supporter, newly uploaded images are shown publicly only after a moderator has checked them.",
|
||||
"bottomTexts.name": "Note that if you change your team's name then someone else can claim the name and URL for their team",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "Automatic",
|
||||
"options.clockFormat.24h": "24-hour",
|
||||
"options.clockFormat.12h": "12-hour",
|
||||
"options.defaultMatchPageTab.rosters": "Rosters",
|
||||
"options.defaultMatchPageTab.join": "Join",
|
||||
"options.defaultMatchPageTab.action": "Action",
|
||||
"options.teamMemberRole.CAPTAIN": "Captain",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Captain",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Pool",
|
||||
"match.password.short": "Pass",
|
||||
"match.hostedBy": "Hosted by",
|
||||
"match.noSplatnetWarning": "One or more players don't have Splatnet, remember to use the pool and pass",
|
||||
"match.room.noRoomHint": "Send the SplatNet room link to the chat or use the Share feature in the SplatNet3 app",
|
||||
"match.room.stalePrompt": "Your room from {{minutes}} minutes ago still up?",
|
||||
"match.room.confirm": "Confirm",
|
||||
"match.cancelMatch": "Cancel match",
|
||||
"match.canceled": "Match canceled",
|
||||
"match.canceled.detail": "Set was canceled as requested by {{requester}} and accepted by {{accepter}}",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "Con borde",
|
||||
"actions.noOutline": "Sin borde",
|
||||
"actions.join": "Unirse",
|
||||
"host": "",
|
||||
"actions.nevermind": "Olvídalo",
|
||||
"actions.clickHere": "Haz clic aquí",
|
||||
"actions.goBack": "Volver atrás",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "Formato de hora",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Desactivar orden automático de potenciadores",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "No permitir invitaciones de usuarios no verificados",
|
||||
"labels.noScreen": "[Accesibilidad] Evitar Pantintalla",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Nota que si cambias el nombre de tu equipo, el nombre y la URL estarán libres para que otro equipo los tome",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "Automático",
|
||||
"options.clockFormat.24h": "24 horas",
|
||||
"options.clockFormat.12h": "12 horas",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitán",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Capitán",
|
||||
"options.teamMemberRole.FRONTLINE": "Delantero",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Grupo",
|
||||
"match.password.short": "Codigo",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Cancelar partido",
|
||||
"match.canceled": "Partido cancelado",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Unirse",
|
||||
"host": "",
|
||||
"actions.nevermind": "Cancelar",
|
||||
"actions.clickHere": "Haga click aquí",
|
||||
"actions.goBack": "Volver",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Nota que si cambias el nombre de tu equipo, el nombre y la URL estarán libres para que otro equipo los tome",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitán",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Delantero",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Canal",
|
||||
"match.password.short": "Contraseña",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Cancelar partido",
|
||||
"match.canceled": "Partido cancelado",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Joindre",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Veuillez noter que si vous changer le nom de l'équipe, quelqu'un d'autre pourra s'emparer de l'ancien nom et URL",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitaine",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "Outlined",
|
||||
"actions.noOutline": "No outline",
|
||||
"actions.join": "Joindre",
|
||||
"host": "",
|
||||
"actions.nevermind": "Laisser tomber",
|
||||
"actions.clickHere": "Click ici",
|
||||
"actions.goBack": "Retourner en arrière",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Veuillez noter que si vous changer le nom de l'équipe, quelqu'un d'autre pourra s'emparer de l'ancien nom et URL",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitaine",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-capitaine",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Canal",
|
||||
"match.password.short": "Mot de passe",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Annuler match",
|
||||
"match.canceled": "Match annulé",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "הצטרפות",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "שימו לב שאם תשנו את שם הצוות שלכם, מישהו אחר יוכל לקחת בעלות על השם ועל כתובת האתר עבור הצוות שלו",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "קפטן",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "קו החזית",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "Contornato",
|
||||
"actions.noOutline": "Nessun contorno",
|
||||
"actions.join": "Entra",
|
||||
"host": "",
|
||||
"actions.nevermind": "Non importa",
|
||||
"actions.clickHere": "Clicca qui",
|
||||
"actions.goBack": "Indietro",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Nota che se cambi il nome del team, qualcun altro può assumere nome e URL per il proprio team",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitano",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Capitano",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Pool",
|
||||
"match.password.short": "Pass",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Cancella match",
|
||||
"match.canceled": "Match cancellato",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"patreon": "Patreon での sendou.ink サポーター",
|
||||
"patreon+": "Patoreon での sendou.ink サポーター+",
|
||||
"xp": "{{xpText}} に到達したことで獲得",
|
||||
"tournament_one": "{{tournament}}を勝利したことで獲得",
|
||||
"tournament_other": "{{tournament}}を(×{{count}})回勝利したことで獲得",
|
||||
"tournament": "{{tournament}}を勝利したことで獲得",
|
||||
"forYourEvent": "イベント専用のバッジ?",
|
||||
"managedBy": "<0></0> によって管理されています",
|
||||
"madeBy": "製作者: <0></0>",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "アウトラインあり",
|
||||
"actions.noOutline": "アウトラインなし",
|
||||
"actions.join": "参加する",
|
||||
"host": "",
|
||||
"actions.nevermind": "やーめた",
|
||||
"actions.clickHere": "ここをクリック",
|
||||
"actions.goBack": "戻る",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "注意: チーム名を変更した場合、他のプレイヤーが変更前の名前と URL を別のチームのために使用することができるようになります。",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "キャプテン",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "キャプテン補佐",
|
||||
"options.teamMemberRole.FRONTLINE": "前衛",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "ヘルプデスク",
|
||||
"match.pool": "プール",
|
||||
"match.password.short": "通過",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "マッチをキャンセルする",
|
||||
"match.canceled": "マッチをキャンセルしました",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "참여하기",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Dołącz",
|
||||
"host": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Uwaga: Jeśli zmienisz imię drużyny, ktoś inny może użyć twoje stare imię i URL",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kapitan",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "",
|
||||
"match.pool": "",
|
||||
"match.password.short": "",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "",
|
||||
"match.canceled": "",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "",
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Entrar",
|
||||
"host": "",
|
||||
"actions.nevermind": "Deixa pra lá...",
|
||||
"actions.clickHere": "Clique aqui",
|
||||
"actions.goBack": "Voltar",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Lembre-se que se você mudar o nome do seu time, alguém pode resgatar o nome e o URL para o time dele(a)",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitão(ã)",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Linha de frente (frontline)",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Helpdesk",
|
||||
"match.pool": "Pool",
|
||||
"match.password.short": "Senha",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Cancelar partida",
|
||||
"match.canceled": "Partida cancelada",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "Обводка",
|
||||
"actions.noOutline": "Без обводки",
|
||||
"actions.join": "Присоединиться",
|
||||
"host": "",
|
||||
"actions.nevermind": "Отмена",
|
||||
"actions.clickHere": "Нажмите здесь",
|
||||
"actions.goBack": "Назад",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "Обратите внимание, что если вы измените название команды, то кто-то другой может забрать себе URL и название для своей команды",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Капитан",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Со-капитан",
|
||||
"options.teamMemberRole.FRONTLINE": "Фронтлайн",
|
||||
|
||||
@@ -124,11 +124,6 @@
|
||||
"match.helpdesk": "Служба поддержки",
|
||||
"match.pool": "Канал",
|
||||
"match.password.short": "Пароль",
|
||||
"match.hostedBy": "",
|
||||
"match.noSplatnetWarning": "",
|
||||
"match.room.noRoomHint": "",
|
||||
"match.room.stalePrompt": "",
|
||||
"match.room.confirm": "",
|
||||
"match.cancelMatch": "Отменить матч",
|
||||
"match.canceled": "Матч был отменён",
|
||||
"match.canceled.detail": "",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"actions.outlined": "描边",
|
||||
"actions.noOutline": "无描边",
|
||||
"actions.join": "加入",
|
||||
"host": "",
|
||||
"actions.nevermind": "反悔",
|
||||
"actions.clickHere": "点击此处",
|
||||
"actions.goBack": "返回",
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.defaultMatchPageTab": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
"labels.noScreen": "",
|
||||
"labels.noSplatnet": "",
|
||||
"labels.spoilerFreeMode": "",
|
||||
"bottomTexts.imageModeration": "",
|
||||
"bottomTexts.name": "请注意,如果您更改了队名,那么其他人便可以使用之前的队名和URL了。",
|
||||
@@ -29,9 +27,6 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.defaultMatchPageTab.rosters": "",
|
||||
"options.defaultMatchPageTab.join": "",
|
||||
"options.defaultMatchPageTab.action": "",
|
||||
"options.teamMemberRole.CAPTAIN": "队长",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "前排",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user