Subbed out

This commit is contained in:
Kalle
2026-04-04 14:15:57 +03:00
parent 97d5e20172
commit 2a19e6bb79
19 changed files with 107 additions and 7 deletions

View File

@@ -2,11 +2,7 @@
& [class*="tabPanel"] {
background-color: var(--color-bg-high);
border-radius: 0 0 var(--radius-box) var(--radius-box);
padding: var(--s-4);
&:has([class*="chatContainer"]) {
padding: var(--s-1) var(--s-4);
}
padding: var(--s-6) var(--s-4);
}
}
@@ -54,6 +50,36 @@
}
}
.subbedOutTrigger {
display: flex;
align-items: center;
gap: var(--s-1-5);
}
.subbedOutIcon {
--subbed-out-icon-size: 24px;
display: flex;
align-items: center;
justify-content: center;
width: var(--subbed-out-icon-size);
height: var(--subbed-out-icon-size);
border-radius: 100%;
background-color: var(--color-bg-higher);
}
.subbedOutPopover {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.subbedOutHeader {
font-size: var(--font-xs);
font-weight: var(--weight-semi);
color: var(--color-text-high);
text-transform: uppercase;
}
.teamOneDot {
border-radius: 100%;
background-color: var(--color-accent);

View File

@@ -1,10 +1,12 @@
import { DoorOpen, Tally5, Users } from "lucide-react";
import { Armchair, DoorOpen, Tally5, Users } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import type * as React from "react";
import { useTranslation } from "react-i18next";
import { Link, useSearchParams } from "react-router";
import { Alert } from "~/components/Alert";
import { Avatar } from "~/components/Avatar";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import invariant from "~/utils/invariant";
import type { CommonUser } from "~/utils/kysely.server";
import { userPage } from "~/utils/urls";
@@ -80,6 +82,8 @@ interface RosterTabTeam {
avatar?: string;
};
members: Array<CommonUser>;
/** Sub user ids i.e. those who are not the current active roster */
subbedOut?: Array<number>;
}
interface MatchRosterTabProps {
@@ -107,6 +111,14 @@ function TeamRoster({
const dotClassName = side === "alpha" ? styles.teamOneDot : styles.teamTwoDot;
const label = side === "alpha" ? "Alpha" : "Bravo";
const subbedOutSet = new Set(team.subbedOut);
const activeMembers = team.members.filter(
(member) => !subbedOutSet.has(member.id),
);
const subbedOutMembers = team.members.filter((member) =>
subbedOutSet.has(member.id),
);
return (
<div className="stack xxs">
{team.team ? (
@@ -127,7 +139,7 @@ function TeamRoster({
) : null}
{team.members.length > 0 ? (
<ul className={styles.rosterMembers}>
{team.members.map((member) => (
{activeMembers.map((member) => (
<li key={member.id}>
<Link
to={userPage(member)}
@@ -138,12 +150,50 @@ function TeamRoster({
</Link>
</li>
))}
{subbedOutMembers.length > 0 ? (
<li>
<SubbedOutPopover members={subbedOutMembers} />
</li>
) : null}
</ul>
) : null}
</div>
);
}
function SubbedOutPopover({ members }: { members: Array<CommonUser> }) {
const { t } = useTranslation(["q"]);
return (
<SendouPopover
trigger={
<SendouButton variant="minimal" size="small">
<div className={styles.subbedOutTrigger}>
<div className={styles.subbedOutIcon}>
<Armchair size={16} />
</div>
+{members.length}
</div>
</SendouButton>
}
>
<div className={styles.subbedOutPopover}>
<div className={styles.subbedOutHeader}>{t("q:match.subbedOut")}</div>
{members.map((member) => (
<Link
key={member.id}
to={userPage(member)}
className="stack horizontal sm items-center"
>
<Avatar user={member} size="xxs" />
<span>{member.username}</span>
</Link>
))}
</div>
</SendouPopover>
);
}
export function MatchActionTab() {
return <SendouTabPanel id={TAB_KEYS.ACTION}>Report content</SendouTabPanel>;
}

View File

@@ -171,7 +171,15 @@ export default function MatchPageTestRoute() {
discordAvatar: null,
customUrl: null,
},
{
id: 9,
username: "Poppy",
discordId: "567",
discordAvatar: null,
customUrl: null,
},
],
subbedOut: [9],
},
{
team: { name: "Question Mark", url: "/t/question-mark" },

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "loss",
"match.screen.ban": "Weapons with {{special}} are not allowed in this match",
"match.screen.allowed": "Weapons with {{special}} are allowed in this match",
"match.subbedOut": "Subbed out",
"preparing.joinQ": "Join the queue",
"tiers.currentCriteria": "Current criteria",
"tiers.info.p1": "For example, Leviathan is the top 5% of players. Diamond is the 85th percentile etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "perdida",
"match.screen.ban": "Armas con {{special}} son prohibidas para este partido",
"match.screen.allowed": "Armas con {{special}} se permiten para este partido",
"match.subbedOut": "",
"preparing.joinQ": "Unirte a la fila",
"tiers.currentCriteria": "Criterios actuales",
"tiers.info.p1": "Por ejemplo, Leviathan se encuentra entre el 5% de los mejores jugadores. Diamond es el percentil 85, etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "perdida",
"match.screen.ban": "Armas con {{special}} son prohibidas para este partido",
"match.screen.allowed": "Armas con {{special}} se permiten para este partido",
"match.subbedOut": "",
"preparing.joinQ": "Unirte a la fila",
"tiers.currentCriteria": "Criterios actuales",
"tiers.info.p1": "Por ejemplo, Leviathan se encuentra entre el 5% de los mejores jugadores. Diamond es el percentil 85, etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "Perdu",
"match.screen.ban": "Les armes avec {{special}} ne sont pas autoriser pendant ce match",
"match.screen.allowed": "Les armes avec {{special}} sont autoriser pendant ce match",
"match.subbedOut": "",
"preparing.joinQ": "Rejoindre la queue",
"tiers.currentCriteria": "Critères actuels",
"tiers.info.p1": "Par exemple, Les Léviathans font partie des 5 % des meilleurs joueurs. Le diamant est le top 15%, etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "sconfitta",
"match.screen.ban": "Armi con {{special}} non sono permesse in questo match",
"match.screen.allowed": "Armi con {{special}} non sono permesse in questo match",
"match.subbedOut": "",
"preparing.joinQ": "Unisciti alla coda",
"tiers.currentCriteria": "Criterio corrente",
"tiers.info.p1": "Per esempio Leviathan è la top 5% dei giocatori. Diamante è l' 85esimo percentile etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "負け",
"match.screen.ban": "{{special}}が付いてる武器はこのマッチでは禁止です",
"match.screen.allowed": "{{special}}が付いてる武器は使用可能です",
"match.subbedOut": "",
"preparing.joinQ": "列に入る",
"tiers.currentCriteria": "現在の基準",
"tiers.info.p1": "例として、Leviathanはプレイヤーの上位5%、Diamondは上位15%",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "",
"match.screen.ban": "",
"match.screen.allowed": "",
"match.subbedOut": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "derrota",
"match.screen.ban": "Armas com o/a {{special}} não são permitidas nessa partida",
"match.screen.allowed": "Armas com o/a {{special}} são permitidas nessa partida",
"match.subbedOut": "",
"preparing.joinQ": "Entrar na fila",
"tiers.currentCriteria": "Critérios atuais",
"tiers.info.p1": "Por exemplo, Leviathan é o top 5% dos jogadores. Diamond é top 15% e etc.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "проигрыш",
"match.screen.ban": "Оружия с {{special}} запрещены в этом матче",
"match.screen.allowed": "Оружия с {{special}} разрешены в этом матче",
"match.subbedOut": "",
"preparing.joinQ": "Присоединиться к очереди",
"tiers.currentCriteria": "Текущие критерии",
"tiers.info.p1": "Например, Leviathan - топ 5% игроков, Diamond - 85 процентиль и т.д.",

View File

@@ -164,6 +164,7 @@
"match.outcome.loss": "负",
"match.screen.ban": "本次对战禁止使用特殊武器为 {{special}} 的武器",
"match.screen.allowed": "本次对战允许使用特殊武器为 {{special}} 的武器",
"match.subbedOut": "",
"preparing.joinQ": "开始匹配",
"tiers.currentCriteria": "当前规则",
"tiers.info.p1": "比如说Leviathan是前5%的玩家Diamond是前15%的玩家。",