New tabs component (#2387)

This commit is contained in:
Kalle
2025-06-10 20:58:41 +03:00
committed by GitHub
parent 97db59821c
commit d6abac6b72
19 changed files with 807 additions and 954 deletions

View File

@@ -1,154 +0,0 @@
import { Tab } from "@headlessui/react";
import clsx from "clsx";
import * as React from "react";
interface NewTabsProps {
tabs: {
label: string;
number?: number;
icon?: React.ReactNode;
hidden?: boolean;
disabled?: boolean;
}[];
content: {
key: string;
element: React.ReactNode;
hidden?: boolean;
unmount?: boolean;
}[];
scrolling?: boolean;
selectedIndex?: number;
defaultIndex?: number;
setSelectedIndex?: (index: number) => void;
/** Don't take space when no tabs to show? */
disappearing?: boolean;
/** Show padding between tabs and content
* @default true
*/
padded?: boolean;
type?: "divider";
sticky?: boolean;
}
export function NewTabs(args: NewTabsProps) {
if (args.type === "divider") {
return <DividerTabs {...args} />;
}
const {
tabs,
content,
scrolling = true,
selectedIndex,
setSelectedIndex,
defaultIndex,
disappearing = false,
padded = true,
} = args;
const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1;
return (
<Tab.Group
selectedIndex={selectedIndex}
onChange={setSelectedIndex}
defaultIndex={defaultIndex}
>
<Tab.List
className={clsx("tab__buttons-container", {
"overflow-x-auto": scrolling,
invisible: cantSwitchTabs && !disappearing,
hidden: cantSwitchTabs && disappearing,
"tab__buttons-container__sticky": args.sticky,
})}
>
{tabs
.filter((t) => !t.hidden)
.map((tab) => {
return (
<Tab
key={tab.label}
className="button tab__button"
data-testid={`tab-${tab.label}`}
disabled={tab.disabled}
>
{tab.icon}
{tab.label}
{typeof tab.number === "number" && tab.number !== 0 && (
<span className="tab__number">{tab.number}</span>
)}
</Tab>
);
})}
</Tab.List>
<Tab.Panels
className={clsx({
"mt-4": padded && (!cantSwitchTabs || !disappearing),
})}
>
{content
.filter((c) => !c.hidden)
.map((c) => {
return (
<Tab.Panel key={c.key} unmount={c.unmount}>
{c.element}
</Tab.Panel>
);
})}
</Tab.Panels>
</Tab.Group>
);
}
function DividerTabs({
tabs,
content,
scrolling = true,
selectedIndex,
setSelectedIndex,
disappearing = false,
}: NewTabsProps) {
const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1;
return (
<Tab.Group selectedIndex={selectedIndex} onChange={setSelectedIndex}>
<Tab.List
className={clsx("divider-tab__buttons-container", {
"overflow-x-auto": scrolling,
invisible: cantSwitchTabs && !disappearing,
hidden: cantSwitchTabs && disappearing,
})}
>
{tabs
.filter((t) => !t.hidden)
.map((tab, i) => {
return (
<React.Fragment key={tab.label}>
<Tab
className="divider-tab__button"
data-testid={`tab-${tab.label}`}
>
{tab.label}
{typeof tab.number === "number" && tab.number !== 0 && (
<span className="ml-1">({tab.number})</span>
)}
</Tab>
{i !== tabs.length - 1 && (
<div className="divider-tab__line-guy" />
)}
</React.Fragment>
);
})}
</Tab.List>
<Tab.Panels
className={clsx({ "mt-4": !cantSwitchTabs || !disappearing })}
>
{content
.filter((c) => !c.hidden)
.map((c) => {
return <Tab.Panel key={c.key}>{c.element}</Tab.Panel>;
})}
</Tab.Panels>
</Tab.Group>
);
}

View File

@@ -1,50 +0,0 @@
import clsx from "clsx";
import type * as React from "react";
// shares styles with SubNav.tsx
export function Tabs({
children,
className,
compact = false,
}: {
children: React.ReactNode;
className?: string;
compact?: boolean;
}) {
return (
<div className={clsx("sub-nav__container", className, { compact })}>
{children}
</div>
);
}
export function Tab({
children,
className,
active,
onClick,
testId,
}: {
children: React.ReactNode;
className?: string;
active: boolean;
onClick: () => void;
testId?: string;
}) {
// TODO: improve semantic html here, maybe could use tab component from Headless UI?
return (
<div
className={clsx("sub-nav__link__container", { active })}
onClick={onClick}
tabIndex={0}
// biome-ignore lint/a11y/useSemanticElements: this component is deprecated
role="button"
aria-pressed="false"
data-testid={testId}
>
<div className={clsx("sub-nav__link", className)}>{children}</div>
<div className="sub-nav__border-guy" />
</div>
);
}

View File

@@ -0,0 +1,60 @@
.tabList {
display: flex;
flex-direction: row;
border-bottom: 2px solid var(--border);
}
.tabList svg {
--icon-size: 16px;
min-width: var(--icon-size);
min-height: var(--icon-size);
max-width: var(--icon-size);
max-height: var(--icon-size);
margin-inline-end: var(--s-1-5);
}
.padded .tabPanel {
padding-block-start: var(--s-4);
}
.disappearing:has(.tabList .tabButton:only-child).padded .tabPanel {
padding-top: 0;
}
.disappearing .tabList:has(.tabButton:only-child) {
display: none;
}
.tabButton {
background-color: transparent;
border: none;
font-size: var(--fonts-xs);
border-radius: 0;
border-bottom: 2px solid transparent;
color: var(--text-lighter);
white-space: nowrap;
flex: 1;
transform: none !important;
}
.tabButton[data-selected] {
border-color: var(--theme);
color: var(--text);
}
.tabButton[data-focus-visible] {
color: var(--theme) !important;
outline: none;
}
.tabNumber {
color: var(--theme);
margin-inline-start: var(--s-2);
}
.sticky {
position: sticky;
top: 47px;
z-index: 1;
background-color: var(--bg);
}

View File

@@ -0,0 +1,114 @@
import clsx from "clsx";
import { Tabs, type TabsProps } from "react-aria-components";
import {
TabList,
type TabListProps,
TabPanel,
type TabPanelProps,
} from "react-aria-components";
import { Tab, type TabProps } from "react-aria-components";
import buttonStyles from "./Button.module.css";
import styles from "./Tabs.module.css";
interface SendouTabsProps extends TabsProps {
/** Should there be padding above the panels. Defaults to true, pass in false if the panel content is managing its own padding. */
padded?: boolean;
/** Hide tabs if only one tab shown? Defaults to true. */
disappearing?: boolean;
}
/**
* Renders a set of accessible tabs using the provided props.
*
* This component is a wrapper around the `Tabs` component, forwarding all props.
*
* @param props - The properties to pass to the underlying `Tabs` component.
* @returns The rendered tab interface.
*
* @url https://react-spectrum.adobe.com/react-aria/Tabs.html
*
* @example
* <SendouTabs>
* <SendouTabList>
* <Tab id="shooter">Shooter</Tab>
* <Tab id="roller">Roller</Tab>
* <Tab id="charger">Charger</Tab>
* </SendouTabList>
* <SendouTabPanel id="shooter">
* Splattershot, Aerospray, etc.
* </SendouTabPanel>
* <SendouTabPanel id="roller">
* Splat Roller, Dynamo Roller, etc.
* </SendouTabPanel>
* <SendouTabPanel id="charger">
* Splat Charger, E-liter, etc.
* </SendouTabPanel>
* </SendouTabs>
*/
export function SendouTabs({
padded = true,
disappearing = true,
className,
...rest
}: SendouTabsProps) {
return (
<Tabs
className={clsx(className, {
[styles.padded]: padded,
[styles.disappearing]: disappearing,
})}
{...rest}
/>
);
}
interface SendouTabProps extends TabProps {
icon?: React.ReactNode;
number?: number;
children?: React.ReactNode;
}
export function SendouTab({ icon, children, number, ...rest }: SendouTabProps) {
return (
<Tab className={clsx(buttonStyles.button, styles.tabButton)} {...rest}>
{icon}
{children}
{typeof number === "number" && number !== 0 && (
<span className={styles.tabNumber}>{number}</span>
)}
</Tab>
);
}
interface SendouTabListProps<T extends object> extends TabListProps<T> {
/** Should overflow-x: auto CSS rule be applied? Defaults to true */
scrolling?: boolean;
sticky?: boolean;
}
export function SendouTabList<T extends object>({
scrolling = true,
sticky,
...rest
}: SendouTabListProps<T>) {
return (
<TabList
className={clsx(styles.tabList, {
"overflow-x-auto": scrolling,
// invisible: cantSwitchTabs && !disappearing,
// hidden: cantSwitchTabs && disappearing,
[styles.sticky]: sticky,
})}
{...rest}
/>
);
}
interface SendouTabPanelProps extends TabPanelProps {
className?: string;
}
export function SendouTabPanel({ className, ...rest }: SendouTabPanelProps) {
return <TabPanel className={clsx(className, styles.tabPanel)} {...rest} />;
}

View File

@@ -12,9 +12,14 @@ import { Avatar } from "~/components/Avatar";
import { Catcher } from "~/components/Catcher";
import { Input } from "~/components/Input";
import { Main } from "~/components/Main";
import { NewTabs } from "~/components/NewTabs";
import { SubmitButton } from "~/components/SubmitButton";
import { SendouButton } from "~/components/elements/Button";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { UserSearch } from "~/components/elements/UserSearch";
import { SearchIcon } from "~/components/icons/Search";
import { FRIEND_CODE_REGEXP_PATTERN } from "~/features/sendouq/q-constants";
@@ -41,26 +46,18 @@ export const meta: MetaFunction = (args) => {
export default function AdminPage() {
return (
<Main>
<NewTabs
tabs={[
{
label: "Actions",
},
{
label: "Friend code look-up",
},
]}
content={[
{
key: "actions",
element: <AdminActions />,
},
{
key: "friend-code-look-up",
element: <FriendCodeLookUp />,
},
]}
/>
<SendouTabs>
<SendouTabList>
<SendouTab id="actions">Actions</SendouTab>
<SendouTab id="friend-code-look-up">Friend code look-up</SendouTab>
</SendouTabList>
<SendouTabPanel id="actions">
<AdminActions />
</SendouTabPanel>
<SendouTabPanel id="friend-code-look-up">
<FriendCodeLookUp />
</SendouTabPanel>
</SendouTabs>
</Main>
);
}

View File

@@ -11,7 +11,12 @@ import { WeaponCombobox } from "~/components/Combobox";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { Table } from "~/components/Table";
import { Tab, Tabs } from "~/components/Tabs";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { BeakerIcon } from "~/components/icons/Beaker";
import { useUser } from "~/features/auth/core/user";
import { useIsMounted } from "~/hooks/useIsMounted";
@@ -255,71 +260,82 @@ function BuildAnalyzerPage() {
</div>
<div className="stack md items-center w-full">
<div className="w-full">
<Tabs className="analyzer__sub-nav" compact>
<Tab
active={focused === 1}
onClick={() => handleChange({ newFocused: 1 })}
testId="build1-tab"
>
{t("analyzer:build1")}
</Tab>
<Tab
active={focused === 2}
onClick={() => handleChange({ newFocused: 2 })}
testId="build2-tab"
>
{t("analyzer:build2")}
</Tab>
<Tab
active={focused === 3}
onClick={() => handleChange({ newFocused: 3 })}
testId="ap-tab"
>
{t("analyzer:compare")}
</Tab>
</Tabs>
{focusedBuild ? (
<AbilitiesSelector
selectedAbilities={focusedBuild}
onChange={(newBuild) => {
const firstBuildIsEmpty = build
.flat()
.every((ability) => ability === "UNKNOWN");
<SendouTabs
selectedKey={`build-${focused === 3 ? "compare" : focused}`}
onSelectionChange={(id) => {
if (id === "build-1") {
handleChange({ newFocused: 1 });
} else if (id === "build-2") {
handleChange({ newFocused: 2 });
} else {
handleChange({ newFocused: 3 });
}
}}
className="analyzer__sub-nav"
>
<SendouTabList>
<SendouTab id="build-1" data-testid="build1-tab">
{t("analyzer:build1")}
</SendouTab>
<SendouTab id="build-2" data-testid="build2-tab">
{t("analyzer:build2")}
</SendouTab>
<SendouTab id="build-compare" data-testid="ap-tab">
{t("analyzer:compare")}
</SendouTab>
</SendouTabList>
{[1, 2].map(
(buildIndex) =>
focusedBuild && (
<SendouTabPanel
id={`build-${buildIndex}`}
key={`build-${buildIndex}`}
>
<AbilitiesSelector
selectedAbilities={focusedBuild}
onChange={(newBuild) => {
const firstBuildIsEmpty = build
.flat()
.every((ability) => ability === "UNKNOWN");
const buildWasEmptied =
!firstBuildIsEmpty &&
newBuild
.flat()
.every((ability) => ability === "UNKNOWN") &&
focused === 1;
const buildWasEmptied =
!firstBuildIsEmpty &&
newBuild
.flat()
.every((ability) => ability === "UNKNOWN") &&
focused === 1;
// if we don't do this the
// build2 would be duplicated
if (buildWasEmptied) {
handleChange({
newBuild: build2,
newBuild2: newBuild,
newFocused: 1,
});
return;
}
// if we don't do this the
// build2 would be duplicated
if (buildWasEmptied) {
handleChange({
newBuild: build2,
newBuild2: newBuild,
newFocused: 1,
});
return;
}
handleChange({
[focused === 1 || firstBuildIsEmpty
? "newBuild"
: "newBuild2"]: newBuild,
newFocused: firstBuildIsEmpty ? 1 : undefined,
});
}}
/>
) : (
<APCompare
abilityPoints={abilityPoints}
abilityPoints2={abilityPoints2}
build={build}
build2={build2}
/>
)}
handleChange({
[focused === 1 || firstBuildIsEmpty
? "newBuild"
: "newBuild2"]: newBuild,
newFocused: firstBuildIsEmpty ? 1 : undefined,
});
}}
/>
</SendouTabPanel>
),
)}
<SendouTabPanel id="build-compare">
<APCompare
abilityPoints={abilityPoints}
abilityPoints2={abilityPoints2}
build={build}
build2={build2}
/>
</SendouTabPanel>
</SendouTabs>
</div>
<EffectsSelector
build={build}

View File

@@ -6,8 +6,13 @@ import { Avatar } from "~/components/Avatar";
import { Divider } from "~/components/Divider";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { NewTabs } from "~/components/NewTabs";
import { SendouButton } from "~/components/elements/Button";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { ArrowRightIcon } from "~/components/icons/ArrowRight";
import { BSKYLikeIcon } from "~/components/icons/BSKYLike";
import { BSKYReplyIcon } from "~/components/icons/BSKYReply";
@@ -177,58 +182,44 @@ function TournamentCards() {
return null;
}
const showSignedUpTab = data.tournaments.participatingFor.length > 0;
const showOrganizerTab = data.tournaments.organizingFor.length > 0;
const showDiscoverTab = data.tournaments.showcase.length > 0;
return (
<div>
<NewTabs
disappearing
padded={false}
tabs={[
{
label: t("front:showcase.tabs.signedUp"),
hidden: data.tournaments.participatingFor.length === 0,
icon: <UsersIcon />,
},
{
label: t("front:showcase.tabs.organizer"),
hidden: data.tournaments.organizingFor.length === 0,
icon: <KeyIcon />,
},
{
label: t("front:showcase.tabs.discover"),
hidden: data.tournaments.showcase.length === 0,
icon: <SearchIcon />,
},
]}
content={[
{
key: "your",
hidden: data.tournaments.participatingFor.length === 0,
element: (
<ShowcaseTournamentScroller
tournaments={data.tournaments.participatingFor}
/>
),
},
{
key: "organizer",
hidden: data.tournaments.organizingFor.length === 0,
element: (
<ShowcaseTournamentScroller
tournaments={data.tournaments.organizingFor}
/>
),
},
{
key: "discover",
hidden: data.tournaments.showcase.length === 0,
element: (
<ShowcaseTournamentScroller
tournaments={data.tournaments.showcase}
/>
),
},
]}
/>
<SendouTabs padded={false}>
<SendouTabList>
{showSignedUpTab ? (
<SendouTab id="signed-up" icon={<UsersIcon />}>
{t("front:showcase.tabs.signedUp")}
</SendouTab>
) : null}
{showOrganizerTab ? (
<SendouTab id="organizer" icon={<KeyIcon />}>
{t("front:showcase.tabs.organizer")}
</SendouTab>
) : null}
{showDiscoverTab ? (
<SendouTab id="discover" icon={<SearchIcon />}>
{t("front:showcase.tabs.discover")}
</SendouTab>
) : null}
</SendouTabList>
<SendouTabPanel id="signed-up">
<ShowcaseTournamentScroller
tournaments={data.tournaments.participatingFor}
/>
</SendouTabPanel>
<SendouTabPanel id="organizer">
<ShowcaseTournamentScroller
tournaments={data.tournaments.organizingFor}
/>
</SendouTabPanel>
<SendouTabPanel id="discover">
<ShowcaseTournamentScroller tournaments={data.tournaments.showcase} />
</SendouTabPanel>
</SendouTabs>
</div>
);
}

View File

@@ -34,7 +34,12 @@ import {
userSubmittedImage,
} from "~/utils/urls";
import { Main } from "../../../components/Main";
import { NewTabs } from "../../../components/NewTabs";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "../../../components/elements/Tabs";
import { ArrowDownOnSquareIcon } from "../../../components/icons/ArrowDownOnSquare";
import { ArrowUpOnSquareIcon } from "../../../components/icons/ArrowUpOnSquare";
import { CheckmarkIcon } from "../../../components/icons/Checkmark";
@@ -107,67 +112,64 @@ export default function ScrimsPage() {
close={() => setScrimToRequestId(undefined)}
/>
) : null}
<NewTabs
sticky
disappearing
defaultIndex={data.posts.owned.length > 0 ? 0 : 2}
tabs={[
{
label: t("scrims:tabs.owned"),
number: data.posts.owned.length,
disabled: !user,
icon: <ArrowDownOnSquareIcon />,
},
{
label: t("scrims:tabs.requests"),
number: data.posts.requested.length,
disabled: !user,
icon: <ArrowUpOnSquareIcon />,
},
{
label: t("scrims:tabs.available"),
number: data.posts.neutral.length,
icon: <MegaphoneIcon />,
},
]}
content={[
{
key: "owned",
element: (
<ScrimsDaySeparatedTables
posts={data.posts.owned}
showDeletePost
showRequestRows
showStatus
/>
),
},
{
key: "requested",
element: (
<ScrimsDaySeparatedTables
posts={data.posts.requested}
requestScrim={setScrimToRequestId}
showStatus
/>
),
},
{
key: "available",
element:
data.posts.neutral.length > 0 ? (
<ScrimsDaySeparatedTables
posts={data.posts.neutral}
requestScrim={setScrimToRequestId}
/>
) : (
<div className="text-lighter text-lg font-semi-bold text-center mt-6">
{t("scrims:noneAvailable")}
</div>
),
},
]}
/>
<SendouTabs
defaultSelectedKey={data.posts.owned.length > 0 ? "owned" : "available"}
>
<SendouTabList sticky>
<SendouTab
id="owned"
isDisabled={!user}
icon={<ArrowDownOnSquareIcon />}
number={data.posts.owned.length}
>
{t("scrims:tabs.owned")}
</SendouTab>
<SendouTab
id="requested"
isDisabled={!user}
icon={<ArrowUpOnSquareIcon />}
number={data.posts.requested.length}
data-testid="requests-scrims-tab"
>
{t("scrims:tabs.requests")}
</SendouTab>
<SendouTab
id="available"
icon={<MegaphoneIcon />}
number={data.posts.neutral.length}
data-testid="available-scrims-tab"
>
{t("scrims:tabs.available")}
</SendouTab>
</SendouTabList>
<SendouTabPanel id="owned">
<ScrimsDaySeparatedTables
posts={data.posts.owned}
showDeletePost
showRequestRows
showStatus
/>
</SendouTabPanel>
<SendouTabPanel id="requested">
<ScrimsDaySeparatedTables
posts={data.posts.requested}
requestScrim={setScrimToRequestId}
showStatus
/>
</SendouTabPanel>
<SendouTabPanel id="available">
{data.posts.neutral.length > 0 ? (
<ScrimsDaySeparatedTables
posts={data.posts.neutral}
requestScrim={setScrimToRequestId}
/>
) : (
<div className="text-lighter text-lg font-semi-bold text-center mt-6">
{t("scrims:noneAvailable")}
</div>
)}
</SendouTabPanel>
</SendouTabs>
<div className="mt-6 text-xs text-center text-lighter">
{t("calendar:inYourTimeZone")}{" "}
{Intl.DateTimeFormat().resolvedOptions().timeZone}

View File

@@ -18,12 +18,17 @@ import { Divider } from "~/components/Divider";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Image, ModeImage, StageImage, WeaponImage } from "~/components/Image";
import { Main } from "~/components/Main";
import { NewTabs } from "~/components/NewTabs";
import { SubmitButton } from "~/components/SubmitButton";
import { LinkButton } from "~/components/elements/Button";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import { SendouSwitch } from "~/components/elements/Switch";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { ArchiveBoxIcon } from "~/components/icons/ArchiveBox";
import { CrossIcon } from "~/components/icons/Cross";
import { DiscordIcon } from "~/components/icons/Discord";
@@ -677,6 +682,12 @@ function BottomSection({
].filter(Boolean) as ChatProps["rooms"];
}, [data.matchChatCode, data.groupChatCode]);
const chatHidden = chatRooms.length === 0;
const [selectedTabKey, setSelectedTabKey] = React.useState<string>(
chatHidden ? "report" : "chat",
);
const ownWeaponsReported = data.rawReportedWeapons?.some(
(rw) => rw.userId === user?.id,
);
@@ -796,8 +807,6 @@ function BottomSection({
<ScreenLegalityInfo ban={data.banScreen} />
) : null;
const chatHidden = chatRooms.length === 0;
if (!showMid && chatHidden) {
return mapListElement;
}
@@ -816,32 +825,29 @@ function BottomSection({
</div>
<div>
<NewTabs
sticky
tabs={[
{
label: t("q:looking.columns.chat"),
number: unseenMessages,
hidden: chatHidden,
},
{
label: t("q:match.tabs.reportScore"),
},
]}
disappearing
content={[
{
key: "chat",
hidden: chatHidden,
element: chatElement,
},
{
key: "report",
element: mapListElement,
unmount: false,
},
]}
/>
<SendouTabs
selectedKey={selectedTabKey}
onSelectionChange={(key) => setSelectedTabKey(key as string)}
>
<SendouTabList sticky>
{!chatHidden && (
<SendouTab id="chat" number={unseenMessages}>
{t("q:looking.columns.chat")}
</SendouTab>
)}
<SendouTab id="report">{t("q:match.tabs.reportScore")}</SendouTab>
</SendouTabList>
<SendouTabPanel id="chat">{chatElement}</SendouTabPanel>
<SendouTabPanel
id="report"
shouldForceMount
className={clsx({
hidden: selectedTabKey !== "report",
})}
>
{mapListElement}
</SendouTabPanel>
</SendouTabs>
</div>
</div>
);

View File

@@ -7,9 +7,14 @@ import { useTranslation } from "react-i18next";
import { Alert } from "~/components/Alert";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { NewTabs } from "~/components/NewTabs";
import { SubmitButton } from "~/components/SubmitButton";
import { LinkButton } from "~/components/elements/Button";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { useUser } from "~/features/auth/core/user";
import { Chat, useChat } from "~/features/chat/components/Chat";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
@@ -319,129 +324,101 @@ function Groups() {
>
{!isMobile ? (
<div>
<NewTabs
disappearing
type="divider"
tabs={[
{
label: t("q:looking.columns.myGroup"),
number: data.groups.own ? data.groups.own.members!.length : 0,
hidden: !data.groups.own,
},
{
label: t("q:looking.columns.chat"),
hidden: !renderChat,
number: unseenMessages,
},
]}
content={[
{
key: "own",
element: ownGroupElement,
},
{
key: "chat",
element: chatElement,
hidden: !data.chatCode,
},
]}
/>
<SendouTabs>
<SendouTabList>
{data.groups.own && (
<SendouTab id="own" number={data.groups.own.members!.length}>
{t("q:looking.columns.myGroup")}
</SendouTab>
)}
{renderChat && (
<SendouTab id="chat" number={unseenMessages}>
{t("q:looking.columns.chat")}
</SendouTab>
)}
</SendouTabList>
<SendouTabPanel id="own">{ownGroupElement}</SendouTabPanel>
{data.chatCode && (
<SendouTabPanel id="chat">{chatElement}</SendouTabPanel>
)}
</SendouTabs>
</div>
) : null}
<div className="q__groups-inner-container">
<NewTabs
disappearing
scrolling={isMobile}
tabs={[
{
label: t("q:looking.columns.groups"),
number: data.groups.neutral.length,
},
{
label: t(
isFullGroup
? "q:looking.columns.challenges"
: "q:looking.columns.invitations",
),
number: data.groups.likesReceived.length,
hidden: !isMobile,
},
{
label: t("q:looking.columns.myGroup"),
number: data.groups.own ? data.groups.own.members!.length : 0,
hidden: !isMobile || !data.groups.own,
},
{
label: t("q:looking.columns.chat"),
hidden: !isMobile || !renderChat,
number: unseenMessages,
},
]}
content={[
{
key: "groups",
element: (
<div className="stack sm">
<ColumnHeader>
{t("q:looking.columns.available")}
</ColumnHeader>
{data.groups.neutral
.filter((group) => isMobile || !group.isLiked)
.map((group) => {
return (
<GroupCard
key={group.id}
group={group}
action={group.isLiked ? "UNLIKE" : "LIKE"}
ownRole={data.role}
isExpired={data.expiryStatus === "EXPIRED"}
showNote
/>
);
})}
</div>
),
},
{
key: "received",
hidden: !isMobile,
element: (
<div className="stack sm">
{!data.groups.own ? <JoinQueuePrompt /> : null}
{data.groups.likesReceived.map((group) => {
const action = () => {
if (!isFullGroup) return "GROUP_UP";
<SendouTabs>
<SendouTabList scrolling={isMobile}>
<SendouTab id="groups" number={data.groups.neutral.length}>
{t("q:looking.columns.groups")}
</SendouTab>
{isMobile && (
<SendouTab
id="received"
number={data.groups.likesReceived.length}
>
{t(
isFullGroup
? "q:looking.columns.challenges"
: "q:looking.columns.invitations",
)}
</SendouTab>
)}
{isMobile && data.groups.own && (
<SendouTab id="own" number={data.groups.own.members!.length}>
{t("q:looking.columns.myGroup")}
</SendouTab>
)}
{isMobile && renderChat && (
<SendouTab id="chat" number={unseenMessages}>
{t("q:looking.columns.chat")}
</SendouTab>
)}
</SendouTabList>
<SendouTabPanel id="groups">
<div className="stack sm">
<ColumnHeader>{t("q:looking.columns.available")}</ColumnHeader>
{data.groups.neutral
.filter((group) => isMobile || !group.isLiked)
.map((group) => {
return (
<GroupCard
key={group.id}
group={group}
action={group.isLiked ? "UNLIKE" : "LIKE"}
ownRole={data.role}
isExpired={data.expiryStatus === "EXPIRED"}
showNote
/>
);
})}
</div>
</SendouTabPanel>
<SendouTabPanel id="received">
<div className="stack sm">
{!data.groups.own ? <JoinQueuePrompt /> : null}
{data.groups.likesReceived.map((group) => {
const action = () => {
if (!isFullGroup) return "GROUP_UP";
if (group.isRechallenge) return "MATCH_UP_RECHALLENGE";
return "MATCH_UP";
};
if (group.isRechallenge) return "MATCH_UP_RECHALLENGE";
return "MATCH_UP";
};
return (
<GroupCard
key={group.id}
group={group}
action={action()}
ownRole={data.role}
isExpired={data.expiryStatus === "EXPIRED"}
showNote
/>
);
})}
</div>
),
},
{
key: "own",
hidden: !isMobile,
element: ownGroupElement,
},
{
key: "chat",
element: chatElement,
hidden: !isMobile || !data.chatCode,
},
]}
/>
return (
<GroupCard
key={group.id}
group={group}
action={action()}
ownRole={data.role}
isExpired={data.expiryStatus === "EXPIRED"}
showNote
/>
);
})}
</div>
</SendouTabPanel>
<SendouTabPanel id="own">{ownGroupElement}</SendouTabPanel>
<SendouTabPanel id="chat">{chatElement}</SendouTabPanel>
</SendouTabs>
</div>
{!isMobile ? (
<div className="stack sm">

View File

@@ -5,10 +5,15 @@ import type { TFunction } from "i18next";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import { NewTabs } from "~/components/NewTabs";
import { SubmitButton } from "~/components/SubmitButton";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { CheckmarkIcon } from "~/components/icons/Checkmark";
import { CrossIcon } from "~/components/icons/Cross";
import { PickIcon } from "~/components/icons/Pick";
@@ -579,10 +584,11 @@ function StartedMatchTabs({
const data = useLoaderData<TournamentMatchLoaderData>();
const [_unseenMessages, setUnseenMessages] = React.useState(0);
const [chatVisible, setChatVisible] = React.useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useSearchParamState({
defaultValue: 0,
const [selectedTabKey, setSelectedTabKey] = useSearchParamState({
defaultValue: "rosters",
name: "tab",
revive: (value) => [0, 1, 2].find((idx) => idx === Number(value)),
revive: (value) =>
["chat", "rosters", "actions"].includes(value) ? value : null,
});
// TODO: resolve this on server (notice it is copy-pasted now)
@@ -677,70 +683,62 @@ function StartedMatchTabs({
return (
<ActionSectionWrapper>
<NewTabs
tabs={[
{
label: "Chat",
number: unseenMessages,
hidden: !showChat,
},
{
label: "Rosters",
},
{
label: presentational ? "Score" : "Actions",
},
]}
disappearing
content={[
{
key: "chat",
hidden: !showChat,
element: (
<>
{showChat ? (
<Chat
rooms={rooms}
users={chatUsers}
className="tournament__chat-container"
messagesContainerClassName="tournament__chat-messages-container pt-0"
chat={chat}
onMount={onChatMount}
onUnmount={onChatUnmount}
missingUserName="???"
/>
) : null}
</>
),
},
{
key: "rosters",
element: <MatchRosters teams={[teams[0].id, teams[1].id]} />,
},
{
key: "report",
unmount: false,
element: (
<MatchActions
// Without the key prop when switching to another match the winnerId is remembered
// which causes "No winning team matching the id" error.
// In addition we want the active roster changing either by the user or by another user
// to reset the state inside. We also want to clear the inputs when a result is submitted
key={matchActionsKey()}
scores={scores}
teams={teams}
position={currentPosition}
result={result}
presentational={
!tournament.canReportScore({ matchId: data.match.id, user })
}
/>
),
},
]}
selectedIndex={selectedTabIndex}
setSelectedIndex={setSelectedTabIndex}
/>
<SendouTabs
selectedKey={selectedTabKey}
onSelectionChange={(key) => setSelectedTabKey(String(key))}
>
<SendouTabList>
{showChat && (
<SendouTab id="chat" number={unseenMessages} data-testid="chat-tab">
Chat
</SendouTab>
)}
<SendouTab id="rosters">Rosters</SendouTab>
<SendouTab id="actions" data-testid="actions-tab">
{presentational ? "Score" : "Actions"}
</SendouTab>
</SendouTabList>
<SendouTabPanel id="chat">
<Chat
rooms={rooms}
users={chatUsers}
className="tournament__chat-container"
messagesContainerClassName="tournament__chat-messages-container pt-0"
chat={chat}
onMount={onChatMount}
onUnmount={onChatUnmount}
missingUserName="???"
/>
</SendouTabPanel>
<SendouTabPanel id="rosters">
<MatchRosters teams={[teams[0].id, teams[1].id]} />
</SendouTabPanel>
<SendouTabPanel
id="actions"
shouldForceMount
className={clsx({
hidden: selectedTabKey !== "actions",
})}
>
<MatchActions
// Without the key prop when switching to another match the winnerId is remembered
// which causes "No winning team matching the id" error.
// In addition we want the active roster changing either by the user or by another user
// to reset the state inside. We also want to clear the inputs when a result is submitted
key={matchActionsKey()}
scores={scores}
teams={teams}
position={currentPosition}
result={result}
presentational={
!tournament.canReportScore({ matchId: data.match.id, user })
}
/>
</SendouTabPanel>
</SendouTabs>
</ActionSectionWrapper>
);
}

View File

@@ -4,13 +4,17 @@ import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { Divider } from "~/components/Divider";
import { Main } from "~/components/Main";
import { NewTabs } from "~/components/NewTabs";
import { Pagination } from "~/components/Pagination";
import { Placement } from "~/components/Placement";
import { LinkButton } from "~/components/elements/Button";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { EditIcon } from "~/components/icons/Edit";
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasPermission } from "~/modules/permissions/hooks";
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
import { metaTags } from "~/utils/remix";
@@ -139,41 +143,32 @@ function InfoTabs() {
const { t } = useTranslation(["org"]);
const data = useLoaderData<typeof loader>();
const hasSocials =
data.organization.socials && data.organization.socials.length > 0;
const hasBadges = data.organization.badges.length > 0;
return (
<div>
<NewTabs
tabs={[
{
label: t("org:edit.form.socialLinks.title"),
disabled:
!data.organization.socials ||
data.organization.socials.length === 0,
},
{
label: t("org:edit.form.members.title"),
},
{
label: t("org:edit.form.badges.title"),
disabled: data.organization.badges.length === 0,
},
]}
content={[
{
element: (
<SocialLinksList links={data.organization.socials ?? []} />
),
key: "socials",
},
{
element: <MembersList />,
key: "members",
},
{
element: <BadgeDisplay badges={data.organization.badges} />,
key: "badges",
},
]}
/>
<SendouTabs>
<SendouTabList>
<SendouTab id="socials" isDisabled={!hasSocials}>
{t("org:edit.form.socialLinks.title")}
</SendouTab>
<SendouTab id="members">{t("org:edit.form.members.title")}</SendouTab>
<SendouTab id="badges" isDisabled={!hasBadges}>
{t("org:edit.form.badges.title")}
</SendouTab>
</SendouTabList>
<SendouTabPanel id="socials">
<SocialLinksList links={data.organization.socials ?? []} />
</SendouTabPanel>
<SendouTabPanel id="members">
<MembersList />
</SendouTabPanel>
<SendouTabPanel id="badges">
<BadgeDisplay badges={data.organization.badges} />
</SendouTabPanel>
</SendouTabs>
</div>
);
}
@@ -232,43 +227,36 @@ function SeriesView({
}) {
const { t } = useTranslation(["org"]);
const hasLeaderboard = Boolean(series.leaderboard);
return (
<div className="stack md">
<SeriesHeader series={series} />
<div>
<NewTabs
disappearing
tabs={[
{
label: t("org:events.tabs.events"),
number: series.eventsCount,
},
{
label: t("org:events.tabs.leaderboard"),
disabled: !series.leaderboard,
},
]}
content={[
{
key: "events",
element: (
<div className="stack lg">
<EventsList showYear />
<EventsPagination series={series} />
</div>
),
},
{
key: "leaderboard",
element: series.leaderboard && (
<EventLeaderboard
leaderboard={series.leaderboard}
ownEntry={series.ownEntry}
/>
),
},
]}
/>
<SendouTabs>
<SendouTabList>
<SendouTab id="events" number={series.eventsCount}>
{t("org:events.tabs.events")}
</SendouTab>
<SendouTab id="leaderboard" isDisabled={!hasLeaderboard}>
{t("org:events.tabs.leaderboard")}
</SendouTab>
</SendouTabList>
<SendouTabPanel id="events">
<div className="stack lg">
<EventsList showYear />
<EventsPagination series={series} />
</div>
</SendouTabPanel>
<SendouTabPanel id="leaderboard">
{hasLeaderboard && (
<EventLeaderboard
leaderboard={series.leaderboard!}
ownEntry={series.ownEntry}
/>
)}
</SendouTabPanel>
</SendouTabs>
</div>
</div>
);
@@ -357,9 +345,6 @@ function EventsList({
}: { showYear?: boolean; filteredByMonth?: boolean }) {
const { t } = useTranslation(["org"]);
const data = useLoaderData<typeof loader>();
const isMounted = useIsMounted();
if (!isMounted) return null;
const now = databaseTimestampNow();

View File

@@ -15,12 +15,17 @@ import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
import { containerClassName } from "~/components/Main";
import { MapPoolStages } from "~/components/MapPoolSelector";
import { NewTabs } from "~/components/NewTabs";
import { Section } from "~/components/Section";
import { SubmitButton } from "~/components/SubmitButton";
import { LinkButton } from "~/components/elements/Button";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { CheckmarkIcon } from "~/components/icons/Checkmark";
import { ClockIcon } from "~/components/icons/Clock";
import { CrossIcon } from "~/components/icons/Cross";
@@ -166,6 +171,9 @@ export default function TournamentRegisterPage() {
);
}
const TABS = ["description", "rules", "register"] as const;
type RegisterPageTab = (typeof TABS)[number];
function TournamentRegisterInfoTabs() {
const user = useUser();
const tournament = useTournament();
@@ -175,16 +183,16 @@ function TournamentRegisterInfoTabs() {
const teamOwned = tournament.ownedTeamByUser(user);
const isRegularMemberOfATeam = teamMemberOf && !teamOwned;
const defaultTab = () => {
if (tournament.hasStarted || !teamOwned) return 0;
const defaultTab = (): RegisterPageTab => {
if (tournament.hasStarted || !teamOwned) return "description";
const registerTab = !tournament.ctx.rules ? 1 : 2;
return registerTab;
return "register";
};
const [tabIndex, setTabIndex] = useSearchParamState({
const [tabKey, setTabKey] = useSearchParamState({
defaultValue: defaultTab(),
name: "tab",
revive: Number,
revive: (val) =>
TABS.includes(val as RegisterPageTab) ? (val as RegisterPageTab) : null,
});
const showAddIGNAlert =
@@ -195,119 +203,110 @@ function TournamentRegisterInfoTabs() {
return (
<div>
<NewTabs
sticky
selectedIndex={tabIndex}
setSelectedIndex={setTabIndex}
tabs={[
{
label: "Description",
},
{
label: "Rules",
hidden: !tournament.ctx.rules,
},
{
label: "Register",
hidden: tournament.hasStarted,
},
]}
disappearing
content={[
{
key: "description",
element: (
<div className="stack lg">
{tournament.ctx.discordUrl ? (
<div className="w-max">
<LinkButton
to={tournament.ctx.discordUrl}
variant="outlined"
size="small"
isExternal
icon={<DiscordIcon />}
>
Join the Discord
</LinkButton>
</div>
) : null}
<SendouTabs
selectedKey={tabKey}
onSelectionChange={(key) => setTabKey(key as RegisterPageTab)}
>
<SendouTabList sticky>
<SendouTab id="description">Description</SendouTab>
{tournament.ctx.rules ? (
<SendouTab id="rules">Rules</SendouTab>
) : null}
{!tournament.hasStarted ? (
<SendouTab id="register" data-testid="register-tab">
Register
</SendouTab>
) : null}
</SendouTabList>
<div className="tournament__info__description">
<Markdown options={{ wrapper: React.Fragment }}>
{tournament.ctx.description ?? ""}
</Markdown>
</div>
<TOPickedMapPoolInfo />
<TiebreakerMapPoolInfo />
<SendouTabPanel id="description">
<div className="stack lg">
{tournament.ctx.discordUrl ? (
<div className="w-max">
<LinkButton
to={tournament.ctx.discordUrl}
variant="outlined"
size="small"
isExternal
icon={<DiscordIcon />}
>
Join the Discord
</LinkButton>
</div>
),
},
{
key: "rules",
hidden: !tournament.ctx.rules,
element: (
<div className="tournament__info__description">
<Markdown options={{ wrapper: React.Fragment }}>
{tournament.ctx.rules ?? ""}
</Markdown>
</div>
),
},
{
key: "register",
hidden: tournament.hasStarted,
element: (
<div className="stack lg">
{isRegularMemberOfATeam ? (
<div className="stack md items-center">
<Alert>{t("tournament:pre.inATeam")}</Alert>
{teamMemberOf && teamMemberOf.checkIns.length === 0 ? (
<FormWithConfirm
dialogHeading={`Leave "${tournament.teamMemberOfByUser(user)?.name}"?`}
fields={[["_action", "LEAVE_TEAM"]]}
submitButtonText="Leave"
) : null}
<div className="tournament__info__description">
<Markdown options={{ wrapper: React.Fragment }}>
{tournament.ctx.description ?? ""}
</Markdown>
</div>
<TOPickedMapPoolInfo />
<TiebreakerMapPoolInfo />
</div>
</SendouTabPanel>
{tournament.ctx.rules ? (
<SendouTabPanel id="rules">
<div className="tournament__info__description">
<Markdown options={{ wrapper: React.Fragment }}>
{tournament.ctx.rules ?? ""}
</Markdown>
</div>
</SendouTabPanel>
) : null}
{!tournament.hasStarted ? (
<SendouTabPanel id="register">
<div className="stack lg">
{isRegularMemberOfATeam ? (
<div className="stack md items-center">
<Alert>{t("tournament:pre.inATeam")}</Alert>
{teamMemberOf && teamMemberOf.checkIns.length === 0 ? (
<FormWithConfirm
dialogHeading={`Leave "${tournament.teamMemberOfByUser(user)?.name}"?`}
fields={[["_action", "LEAVE_TEAM"]]}
submitButtonText="Leave"
>
<SendouButton
className="build__small-text"
variant="minimal-destructive"
type="submit"
>
<SendouButton
className="build__small-text"
variant="minimal-destructive"
type="submit"
>
Leave the team
</SendouButton>
</FormWithConfirm>
) : null}
</div>
) : showAddIGNAlert ? (
<div>
<Alert variation="WARNING">
<div className="stack horizontal sm items-center flex-wrap justify-center text-center">
This tournament requires you to have an in-game name set{" "}
<LinkButton to={userEditProfilePage(user)} size="small">
Edit profile
</LinkButton>
</div>
</Alert>
</div>
) : (
<RegistrationForms />
)}
{user &&
!tournament.teamMemberOfByUser(user) &&
tournament.canAddNewSubPost &&
!showAddIGNAlert &&
!tournament.hasStarted ? (
<Link
to={tournamentSubsPage(tournament.ctx.id)}
className="text-xs text-center"
>
{t("tournament:pre.sub.prompt")}
</Link>
) : null}
</div>
),
},
]}
/>
Leave the team
</SendouButton>
</FormWithConfirm>
) : null}
</div>
) : showAddIGNAlert ? (
<div>
<Alert variation="WARNING">
<div className="stack horizontal sm items-center flex-wrap justify-center text-center">
This tournament requires you to have an in-game name set{" "}
<LinkButton to={userEditProfilePage(user)} size="small">
Edit profile
</LinkButton>
</div>
</Alert>
</div>
) : (
<RegistrationForms />
)}
{user &&
!tournament.teamMemberOfByUser(user) &&
tournament.canAddNewSubPost &&
!showAddIGNAlert &&
!tournament.hasStarted ? (
<Link
to={tournamentSubsPage(tournament.ctx.id)}
className="text-xs text-center"
>
{t("tournament:pre.sub.prompt")}
</Link>
) : null}
</div>
</SendouTabPanel>
) : null}
</SendouTabs>
</div>
);
}

View File

@@ -18,9 +18,14 @@ import {
} from "~/components/Image";
import { Pagination } from "~/components/Pagination";
import { SubNav, SubNavLink } from "~/components/SubNav";
import { Tab, Tabs } from "~/components/Tabs";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { AlertIcon } from "~/components/icons/Alert";
import { TopTenPlayer } from "~/features/leaderboards/components/TopTenPlayer";
import { playerTopTenPlacement } from "~/features/leaderboards/leaderboards-utils";
@@ -436,49 +441,52 @@ function StageWeaponUsageStats(props: {
<ModeImage mode={props.modeShort} width={18} />
{t(`game-misc:STAGE_${props.stageId}`)}
</div>
<Tabs compact className="mb-0">
<Tab active={tab === "SELF"} onClick={() => setTab("SELF")}>
{t("user:seasons.tabs.self")}
</Tab>
<Tab active={tab === "MATE"} onClick={() => setTab("MATE")}>
{t("user:seasons.tabs.teammates")}
</Tab>
<Tab active={tab === "ENEMY"} onClick={() => setTab("ENEMY")}>
{t("user:seasons.tabs.opponents")}
</Tab>
</Tabs>
<div className="u__season__weapon-usage__weapons-container">
{usages.map((u) => {
const winrate = cutToNDecimalPlaces(
(u.wins / (u.wins + u.losses)) * 100,
);
<SendouTabs
selectedKey={tab}
onSelectionChange={(id) => setTab(id as "SELF" | "MATE" | "ENEMY")}
>
<SendouTabList>
<SendouTab id="SELF">{t("user:seasons.tabs.self")}</SendouTab>
<SendouTab id="MATE">{t("user:seasons.tabs.teammates")}</SendouTab>
<SendouTab id="ENEMY">{t("user:seasons.tabs.opponents")}</SendouTab>
</SendouTabList>
{["SELF", "MATE", "ENEMY"].map((id) => (
<SendouTabPanel id={id} key={id}>
<div className="u__season__weapon-usage__weapons-container">
{usages.map((u) => {
const winrate = cutToNDecimalPlaces(
(u.wins / (u.wins + u.losses)) * 100,
);
return (
<div key={u.weaponSplId}>
<WeaponImage
weaponSplId={u.weaponSplId}
variant="build"
width={48}
className="u__season__weapon-usage__weapon"
/>
<div
className={clsx("text-xs font-bold", {
"text-success": winrate >= 50,
"text-warning": winrate < 50,
})}
>
{winrate}%
</div>
<div className="text-xs">
{u.wins} {t("user:seasons.win.short")}
</div>
<div className="text-xs">
{u.losses} {t("user:seasons.loss.short")}
</div>
return (
<div key={u.weaponSplId}>
<WeaponImage
weaponSplId={u.weaponSplId}
variant="build"
width={48}
className="u__season__weapon-usage__weapon"
/>
<div
className={clsx("text-xs font-bold", {
"text-success": winrate >= 50,
"text-warning": winrate < 50,
})}
>
{winrate}%
</div>
<div className="text-xs">
{u.wins} {t("user:seasons.win.short")}
</div>
<div className="text-xs">
{u.losses} {t("user:seasons.loss.short")}
</div>
</div>
);
})}
</div>
);
})}
</div>
</SendouTabPanel>
))}
</SendouTabs>
</div>
);
}

View File

@@ -384,102 +384,6 @@ abbr[title] {
visibility: initial;
}
.tab__buttons-container {
display: flex;
flex-direction: row;
border-bottom: 2px solid var(--border);
}
.tab__buttons-container svg {
--icon-size: 16px;
min-width: var(--icon-size);
min-height: var(--icon-size);
max-width: var(--icon-size);
max-height: var(--icon-size);
margin-inline-end: var(--s-1-5);
}
.tab__button {
background-color: transparent;
border: none;
font-size: var(--fonts-xs);
border-radius: 0;
border-bottom: 2px solid transparent;
color: var(--text-lighter);
white-space: nowrap;
flex: 1;
}
.divider-tab__buttons-container {
text-align: center;
display: flex;
align-items: center;
}
.tab__buttons-container__sticky {
position: sticky;
top: 47px;
z-index: 1;
background-color: var(--bg);
}
.divider-tab__buttons-container::before,
.divider-tab__buttons-container::after {
flex: 1;
content: "";
padding: 2px;
background-color: var(--theme-transparent);
margin: 5px;
border-radius: var(--rounded);
}
.divider-tab__button {
background-color: transparent;
font-size: var(--fonts-xxs);
font-weight: var(--semi-bold);
text-transform: uppercase;
color: var(--text-lighter);
border: 0;
padding: 0;
line-height: 17.36px;
}
.divider-tab__button[data-headlessui-state="selected"] {
color: var(--theme);
}
.divider-tab__line-guy {
background-color: var(--theme-transparent);
border-radius: var(--rounded);
height: 4px;
width: 20.75px;
margin: 5px;
}
.divider-tab__button:active {
transform: initial;
}
.tab__button:active {
transform: initial;
}
.tab__number {
color: var(--theme);
margin-inline-start: var(--s-2);
}
.divider-tab__button:focus-visible,
.tab__button:focus-visible {
color: var(--theme) !important;
outline: none;
}
.tab__button[data-headlessui-state="selected"] {
border-color: var(--theme);
color: var(--text);
}
.react-aria-Button.info-popover__trigger {
border: 2px solid var(--bg-lightest);
border-radius: 100%;

View File

@@ -61,12 +61,12 @@ test.describe("Scrims", () => {
url: scrimsPage(),
});
await page.getByTestId("tab-Available").click();
await page.getByTestId("available-scrims-tab").click();
await page.getByRole("button", { name: "Request" }).first().click();
await submit(page);
await page.getByTestId("tab-Requests").click();
await page.getByTestId("requests-scrims-tab").click();
const cancelRequestButton = page.getByRole("button", {
name: "Cancel",

View File

@@ -47,7 +47,7 @@ const reportResult = async ({
await page.getByTestId("points-input-2").fill(String(points[1]));
};
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
if (
sidesWithMoreThanFourPlayers.includes("first") &&
@@ -145,7 +145,7 @@ test.describe("Tournament bracket", () => {
});
await expect(page.getByTestId("active-roster-needed-text")).toBeVisible();
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("player-checkbox-0").last().click();
await page.getByTestId("player-checkbox-1").last().click();
@@ -161,7 +161,7 @@ test.describe("Tournament bracket", () => {
});
await isNotVisible(page.getByTestId("active-roster-needed-text"));
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("edit-active-roster-button").click();
await page.getByTestId("player-checkbox-3").last().click();
await page.getByTestId("player-checkbox-4").last().click();
@@ -581,7 +581,7 @@ test.describe("Tournament bracket", () => {
points: [100, 0],
});
await page.getByTestId("tab-Score").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("revise-button").click();
await page.getByTestId("player-checkbox-3").first().click();
await page.getByTestId("player-checkbox-4").first().click();
@@ -972,7 +972,7 @@ test.describe("Tournament bracket", () => {
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("pick-ban-button").first().click();
await page.getByTestId("submit-button").click();
@@ -990,7 +990,7 @@ test.describe("Tournament bracket", () => {
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("winner-radio-2").click();
await page.getByTestId("points-input-2").fill("100");
await page.getByTestId("report-score-button").click();
@@ -1007,7 +1007,7 @@ test.describe("Tournament bracket", () => {
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("tab-Actions").click();
await page.getByTestId("actions-tab").click();
await page.getByTestId("winner-radio-1").click();
await page.getByTestId("points-input-1").fill("100");
await page.getByTestId("report-score-button").click();

View File

@@ -135,6 +135,6 @@ test.describe("Tournament staff", () => {
});
await expect(roomPassSelector).toBeVisible();
await expect(page.getByTestId("tab-Chat")).toBeVisible();
await expect(page.getByTestId("chat-tab")).toBeVisible();
});
});

View File

@@ -59,7 +59,7 @@ test.describe("Tournament", () => {
url: tournamentPage(1),
});
await page.getByTestId("tab-Register").click();
await page.getByRole("tab", { name: "Register" }).click();
await page.getByLabel("Pick-up name").fill("Chimera");
await page.getByTestId("save-team-button").click();