diff --git a/app/components/NewTabs.tsx b/app/components/NewTabs.tsx deleted file mode 100644 index fde1d004c..000000000 --- a/app/components/NewTabs.tsx +++ /dev/null @@ -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 ; - } - - const { - tabs, - content, - scrolling = true, - selectedIndex, - setSelectedIndex, - defaultIndex, - disappearing = false, - padded = true, - } = args; - - const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; - - return ( - - - {tabs - .filter((t) => !t.hidden) - .map((tab) => { - return ( - - {tab.icon} - {tab.label} - {typeof tab.number === "number" && tab.number !== 0 && ( - {tab.number} - )} - - ); - })} - - - {content - .filter((c) => !c.hidden) - .map((c) => { - return ( - - {c.element} - - ); - })} - - - ); -} - -function DividerTabs({ - tabs, - content, - scrolling = true, - selectedIndex, - setSelectedIndex, - disappearing = false, -}: NewTabsProps) { - const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; - - return ( - - - {tabs - .filter((t) => !t.hidden) - .map((tab, i) => { - return ( - - - {tab.label} - {typeof tab.number === "number" && tab.number !== 0 && ( - ({tab.number}) - )} - - {i !== tabs.length - 1 && ( -
- )} - - ); - })} - - - {content - .filter((c) => !c.hidden) - .map((c) => { - return {c.element}; - })} - - - ); -} diff --git a/app/components/Tabs.tsx b/app/components/Tabs.tsx deleted file mode 100644 index 96ec8660b..000000000 --- a/app/components/Tabs.tsx +++ /dev/null @@ -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 ( -
- {children} -
- ); -} - -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 ( -
-
{children}
-
-
- ); -} diff --git a/app/components/elements/Tabs.module.css b/app/components/elements/Tabs.module.css new file mode 100644 index 000000000..02b604d72 --- /dev/null +++ b/app/components/elements/Tabs.module.css @@ -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); +} diff --git a/app/components/elements/Tabs.tsx b/app/components/elements/Tabs.tsx new file mode 100644 index 000000000..9542cc042 --- /dev/null +++ b/app/components/elements/Tabs.tsx @@ -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 + * + * + * Shooter + * Roller + * Charger + * + * + * Splattershot, Aerospray, etc. + * + * + * Splat Roller, Dynamo Roller, etc. + * + * + * Splat Charger, E-liter, etc. + * + * + */ +export function SendouTabs({ + padded = true, + disappearing = true, + className, + ...rest +}: SendouTabsProps) { + return ( + + ); +} + +interface SendouTabProps extends TabProps { + icon?: React.ReactNode; + number?: number; + children?: React.ReactNode; +} + +export function SendouTab({ icon, children, number, ...rest }: SendouTabProps) { + return ( + + {icon} + {children} + {typeof number === "number" && number !== 0 && ( + {number} + )} + + ); +} + +interface SendouTabListProps extends TabListProps { + /** Should overflow-x: auto CSS rule be applied? Defaults to true */ + scrolling?: boolean; + sticky?: boolean; +} + +export function SendouTabList({ + scrolling = true, + sticky, + ...rest +}: SendouTabListProps) { + return ( + + ); +} + +interface SendouTabPanelProps extends TabPanelProps { + className?: string; +} + +export function SendouTabPanel({ className, ...rest }: SendouTabPanelProps) { + return ; +} diff --git a/app/features/admin/routes/admin.tsx b/app/features/admin/routes/admin.tsx index 01f2f4a2f..61a6d2f0d 100644 --- a/app/features/admin/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -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 (
- , - }, - { - key: "friend-code-look-up", - element: , - }, - ]} - /> + + + Actions + Friend code look-up + + + + + + + +
); } diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx index c392fa54e..bf3372260 100644 --- a/app/features/build-analyzer/routes/analyzer.tsx +++ b/app/features/build-analyzer/routes/analyzer.tsx @@ -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() {
- - handleChange({ newFocused: 1 })} - testId="build1-tab" - > - {t("analyzer:build1")} - - handleChange({ newFocused: 2 })} - testId="build2-tab" - > - {t("analyzer:build2")} - - handleChange({ newFocused: 3 })} - testId="ap-tab" - > - {t("analyzer:compare")} - - - {focusedBuild ? ( - { - const firstBuildIsEmpty = build - .flat() - .every((ability) => ability === "UNKNOWN"); + { + if (id === "build-1") { + handleChange({ newFocused: 1 }); + } else if (id === "build-2") { + handleChange({ newFocused: 2 }); + } else { + handleChange({ newFocused: 3 }); + } + }} + className="analyzer__sub-nav" + > + + + {t("analyzer:build1")} + + + {t("analyzer:build2")} + + + {t("analyzer:compare")} + + + {[1, 2].map( + (buildIndex) => + focusedBuild && ( + + { + 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, - }); - }} - /> - ) : ( - - )} + handleChange({ + [focused === 1 || firstBuildIsEmpty + ? "newBuild" + : "newBuild2"]: newBuild, + newFocused: firstBuildIsEmpty ? 1 : undefined, + }); + }} + /> + + ), + )} + + + +
0; + const showOrganizerTab = data.tournaments.organizingFor.length > 0; + const showDiscoverTab = data.tournaments.showcase.length > 0; + return (
- , - }, - { - label: t("front:showcase.tabs.organizer"), - hidden: data.tournaments.organizingFor.length === 0, - icon: , - }, - { - label: t("front:showcase.tabs.discover"), - hidden: data.tournaments.showcase.length === 0, - icon: , - }, - ]} - content={[ - { - key: "your", - hidden: data.tournaments.participatingFor.length === 0, - element: ( - - ), - }, - { - key: "organizer", - hidden: data.tournaments.organizingFor.length === 0, - element: ( - - ), - }, - { - key: "discover", - hidden: data.tournaments.showcase.length === 0, - element: ( - - ), - }, - ]} - /> + + + {showSignedUpTab ? ( + }> + {t("front:showcase.tabs.signedUp")} + + ) : null} + {showOrganizerTab ? ( + }> + {t("front:showcase.tabs.organizer")} + + ) : null} + {showDiscoverTab ? ( + }> + {t("front:showcase.tabs.discover")} + + ) : null} + + + + + + + + + + +
); } diff --git a/app/features/scrims/routes/scrims.tsx b/app/features/scrims/routes/scrims.tsx index 40c81f0f7..587952174 100644 --- a/app/features/scrims/routes/scrims.tsx +++ b/app/features/scrims/routes/scrims.tsx @@ -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} - 0 ? 0 : 2} - tabs={[ - { - label: t("scrims:tabs.owned"), - number: data.posts.owned.length, - disabled: !user, - icon: , - }, - { - label: t("scrims:tabs.requests"), - number: data.posts.requested.length, - disabled: !user, - icon: , - }, - { - label: t("scrims:tabs.available"), - number: data.posts.neutral.length, - icon: , - }, - ]} - content={[ - { - key: "owned", - element: ( - - ), - }, - { - key: "requested", - element: ( - - ), - }, - { - key: "available", - element: - data.posts.neutral.length > 0 ? ( - - ) : ( -
- {t("scrims:noneAvailable")} -
- ), - }, - ]} - /> + 0 ? "owned" : "available"} + > + + } + number={data.posts.owned.length} + > + {t("scrims:tabs.owned")} + + } + number={data.posts.requested.length} + data-testid="requests-scrims-tab" + > + {t("scrims:tabs.requests")} + + } + number={data.posts.neutral.length} + data-testid="available-scrims-tab" + > + {t("scrims:tabs.available")} + + + + + + + + + + {data.posts.neutral.length > 0 ? ( + + ) : ( +
+ {t("scrims:noneAvailable")} +
+ )} +
+
{t("calendar:inYourTimeZone")}{" "} {Intl.DateTimeFormat().resolvedOptions().timeZone} diff --git a/app/features/sendouq-match/routes/q.match.$id.tsx b/app/features/sendouq-match/routes/q.match.$id.tsx index 8e8ec0615..b863eba78 100644 --- a/app/features/sendouq-match/routes/q.match.$id.tsx +++ b/app/features/sendouq-match/routes/q.match.$id.tsx @@ -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( + chatHidden ? "report" : "chat", + ); + const ownWeaponsReported = data.rawReportedWeapons?.some( (rw) => rw.userId === user?.id, ); @@ -796,8 +807,6 @@ function BottomSection({ ) : null; - const chatHidden = chatRooms.length === 0; - if (!showMid && chatHidden) { return mapListElement; } @@ -816,32 +825,29 @@ function BottomSection({
- + setSelectedTabKey(key as string)} + > + + {!chatHidden && ( + + {t("q:looking.columns.chat")} + + )} + {t("q:match.tabs.reportScore")} + + {chatElement} + + {mapListElement} + +
); diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index c1cc4420b..20714ed5d 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -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 ? (
- + + + {data.groups.own && ( + + {t("q:looking.columns.myGroup")} + + )} + {renderChat && ( + + {t("q:looking.columns.chat")} + + )} + + {ownGroupElement} + {data.chatCode && ( + {chatElement} + )} +
) : null}
- - - {t("q:looking.columns.available")} - - {data.groups.neutral - .filter((group) => isMobile || !group.isLiked) - .map((group) => { - return ( - - ); - })} -
- ), - }, - { - key: "received", - hidden: !isMobile, - element: ( -
- {!data.groups.own ? : null} - {data.groups.likesReceived.map((group) => { - const action = () => { - if (!isFullGroup) return "GROUP_UP"; + + + + {t("q:looking.columns.groups")} + + {isMobile && ( + + {t( + isFullGroup + ? "q:looking.columns.challenges" + : "q:looking.columns.invitations", + )} + + )} + {isMobile && data.groups.own && ( + + {t("q:looking.columns.myGroup")} + + )} + {isMobile && renderChat && ( + + {t("q:looking.columns.chat")} + + )} + + +
+ {t("q:looking.columns.available")} + {data.groups.neutral + .filter((group) => isMobile || !group.isLiked) + .map((group) => { + return ( + + ); + })} +
+
+ +
+ {!data.groups.own ? : 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 ( - - ); - })} -
- ), - }, - { - key: "own", - hidden: !isMobile, - element: ownGroupElement, - }, - { - key: "chat", - element: chatElement, - hidden: !isMobile || !data.chatCode, - }, - ]} - /> + return ( + + ); + })} +
+ + {ownGroupElement} + {chatElement} +
{!isMobile ? (
diff --git a/app/features/tournament-bracket/components/StartedMatch.tsx b/app/features/tournament-bracket/components/StartedMatch.tsx index bf9c2dc58..0e0180823 100644 --- a/app/features/tournament-bracket/components/StartedMatch.tsx +++ b/app/features/tournament-bracket/components/StartedMatch.tsx @@ -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(); 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 ( - - {showChat ? ( - - ) : null} - - ), - }, - { - key: "rosters", - element: , - }, - { - key: "report", - unmount: false, - element: ( - - ), - }, - ]} - selectedIndex={selectedTabIndex} - setSelectedIndex={setSelectedTabIndex} - /> + setSelectedTabKey(String(key))} + > + + {showChat && ( + + Chat + + )} + Rosters + + {presentational ? "Score" : "Actions"} + + + + + + + + + + + + + + + ); } diff --git a/app/features/tournament-organization/routes/org.$slug.tsx b/app/features/tournament-organization/routes/org.$slug.tsx index b94f46a15..21b8a1ff0 100644 --- a/app/features/tournament-organization/routes/org.$slug.tsx +++ b/app/features/tournament-organization/routes/org.$slug.tsx @@ -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(); + const hasSocials = + data.organization.socials && data.organization.socials.length > 0; + const hasBadges = data.organization.badges.length > 0; + return (
- - ), - key: "socials", - }, - { - element: , - key: "members", - }, - { - element: , - key: "badges", - }, - ]} - /> + + + + {t("org:edit.form.socialLinks.title")} + + {t("org:edit.form.members.title")} + + {t("org:edit.form.badges.title")} + + + + + + + + + + + +
); } @@ -232,43 +227,36 @@ function SeriesView({ }) { const { t } = useTranslation(["org"]); + const hasLeaderboard = Boolean(series.leaderboard); + return (
- - - -
- ), - }, - { - key: "leaderboard", - element: series.leaderboard && ( - - ), - }, - ]} - /> + + + + {t("org:events.tabs.events")} + + + {t("org:events.tabs.leaderboard")} + + + +
+ + +
+
+ + {hasLeaderboard && ( + + )} + +
); @@ -357,9 +345,6 @@ function EventsList({ }: { showYear?: boolean; filteredByMonth?: boolean }) { const { t } = useTranslation(["org"]); const data = useLoaderData(); - const isMounted = useIsMounted(); - - if (!isMounted) return null; const now = databaseTimestampNow(); diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index 8556ad9dd..2ffe8c94a 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -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 (
- - {tournament.ctx.discordUrl ? ( -
- } - > - Join the Discord - -
- ) : null} + setTabKey(key as RegisterPageTab)} + > + + Description + {tournament.ctx.rules ? ( + Rules + ) : null} + {!tournament.hasStarted ? ( + + Register + + ) : null} + -
- - {tournament.ctx.description ?? ""} - -
- - + +
+ {tournament.ctx.discordUrl ? ( +
+ } + > + Join the Discord +
- ), - }, - { - key: "rules", - hidden: !tournament.ctx.rules, - element: ( -
- - {tournament.ctx.rules ?? ""} - -
- ), - }, - { - key: "register", - hidden: tournament.hasStarted, - element: ( -
- {isRegularMemberOfATeam ? ( -
- {t("tournament:pre.inATeam")} - {teamMemberOf && teamMemberOf.checkIns.length === 0 ? ( - + + {tournament.ctx.description ?? ""} + +
+ + +
+ + + {tournament.ctx.rules ? ( + +
+ + {tournament.ctx.rules ?? ""} + +
+
+ ) : null} + + {!tournament.hasStarted ? ( + +
+ {isRegularMemberOfATeam ? ( +
+ {t("tournament:pre.inATeam")} + {teamMemberOf && teamMemberOf.checkIns.length === 0 ? ( + + - - Leave the team - - - ) : null} -
- ) : showAddIGNAlert ? ( -
- -
- This tournament requires you to have an in-game name set{" "} - - Edit profile - -
-
-
- ) : ( - - )} - {user && - !tournament.teamMemberOfByUser(user) && - tournament.canAddNewSubPost && - !showAddIGNAlert && - !tournament.hasStarted ? ( - - {t("tournament:pre.sub.prompt")} - - ) : null} -
- ), - }, - ]} - /> + Leave the team + + + ) : null} +
+ ) : showAddIGNAlert ? ( +
+ +
+ This tournament requires you to have an in-game name set{" "} + + Edit profile + +
+
+
+ ) : ( + + )} + {user && + !tournament.teamMemberOfByUser(user) && + tournament.canAddNewSubPost && + !showAddIGNAlert && + !tournament.hasStarted ? ( + + {t("tournament:pre.sub.prompt")} + + ) : null} +
+ + ) : null} + ); } diff --git a/app/features/user-page/routes/u.$identifier.seasons.tsx b/app/features/user-page/routes/u.$identifier.seasons.tsx index 739478667..e3eb87716 100644 --- a/app/features/user-page/routes/u.$identifier.seasons.tsx +++ b/app/features/user-page/routes/u.$identifier.seasons.tsx @@ -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: { {t(`game-misc:STAGE_${props.stageId}`)} - - setTab("SELF")}> - {t("user:seasons.tabs.self")} - - setTab("MATE")}> - {t("user:seasons.tabs.teammates")} - - setTab("ENEMY")}> - {t("user:seasons.tabs.opponents")} - - -
- {usages.map((u) => { - const winrate = cutToNDecimalPlaces( - (u.wins / (u.wins + u.losses)) * 100, - ); + setTab(id as "SELF" | "MATE" | "ENEMY")} + > + + {t("user:seasons.tabs.self")} + {t("user:seasons.tabs.teammates")} + {t("user:seasons.tabs.opponents")} + + {["SELF", "MATE", "ENEMY"].map((id) => ( + +
+ {usages.map((u) => { + const winrate = cutToNDecimalPlaces( + (u.wins / (u.wins + u.losses)) * 100, + ); - return ( -
- -
= 50, - "text-warning": winrate < 50, - })} - > - {winrate}% -
-
- {u.wins} {t("user:seasons.win.short")} -
-
- {u.losses} {t("user:seasons.loss.short")} -
+ return ( +
+ +
= 50, + "text-warning": winrate < 50, + })} + > + {winrate}% +
+
+ {u.wins} {t("user:seasons.win.short")} +
+
+ {u.losses} {t("user:seasons.loss.short")} +
+
+ ); + })}
- ); - })} -
+
+ ))} +
); } diff --git a/app/styles/common.css b/app/styles/common.css index 66bedbcdc..40c3cd233 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -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%; diff --git a/e2e/scrims.spec.ts b/e2e/scrims.spec.ts index 02cbde73f..0dc4de9e1 100644 --- a/e2e/scrims.spec.ts +++ b/e2e/scrims.spec.ts @@ -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", diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index 8eba3fd79..ab9023df8 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -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(); diff --git a/e2e/tournament-staff.spec.ts b/e2e/tournament-staff.spec.ts index a61ef573f..0471b6a02 100644 --- a/e2e/tournament-staff.spec.ts +++ b/e2e/tournament-staff.spec.ts @@ -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(); }); }); diff --git a/e2e/tournament.spec.ts b/e2e/tournament.spec.ts index 3e9663231..45f7ff1e4 100644 --- a/e2e/tournament.spec.ts +++ b/e2e/tournament.spec.ts @@ -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();