mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-13 14:46:10 -05:00
New tabs component (#2387)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
60
app/components/elements/Tabs.module.css
Normal file
60
app/components/elements/Tabs.module.css
Normal 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);
|
||||
}
|
||||
114
app/components/elements/Tabs.tsx
Normal file
114
app/components/elements/Tabs.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user