mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Refactor several to CSS Modules
This commit is contained in:
parent
4c0ff39da2
commit
0193edce1d
34
app/components/Main.module.css
Normal file
34
app/components/Main.module.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-block: var(--s-4) var(--s-32);
|
||||
}
|
||||
|
||||
.normal {
|
||||
width: 100%;
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding-inline: var(--s-3);
|
||||
min-height: 75vh;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 100%;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (display-mode: standalone) {
|
||||
.main {
|
||||
padding-block-start: env(safe-area-inset-top);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
|
|||
import clsx from "clsx";
|
||||
import type * as React from "react";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import styles from "./Main.module.css";
|
||||
|
||||
export const Main = ({
|
||||
children,
|
||||
|
|
@ -26,20 +27,20 @@ export const Main = ({
|
|||
!isRouteErrorResponse(error);
|
||||
|
||||
return (
|
||||
<div className="layout__main-container">
|
||||
<div className={styles.container}>
|
||||
<main
|
||||
className={
|
||||
classNameOverwrite
|
||||
? clsx(classNameOverwrite, {
|
||||
[containerClassName("narrow")]: halfWidth,
|
||||
[styles.narrow]: halfWidth,
|
||||
"pt-8-forced": showLeaderboard,
|
||||
})
|
||||
: clsx(
|
||||
"layout__main",
|
||||
containerClassName("normal"),
|
||||
styles.main,
|
||||
styles.normal,
|
||||
{
|
||||
[containerClassName("narrow")]: halfWidth,
|
||||
[containerClassName("wide")]: bigger,
|
||||
[styles.narrow]: halfWidth,
|
||||
[styles.wide]: bigger,
|
||||
"pt-8-forced": showLeaderboard,
|
||||
},
|
||||
className,
|
||||
|
|
@ -53,14 +54,16 @@ export const Main = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { styles as mainStyles };
|
||||
|
||||
export const containerClassName = (width: "narrow" | "normal" | "wide") => {
|
||||
if (width === "narrow") {
|
||||
return "half-width";
|
||||
return styles.narrow;
|
||||
}
|
||||
|
||||
if (width === "wide") {
|
||||
return "bigger";
|
||||
return styles.wide;
|
||||
}
|
||||
|
||||
return "main";
|
||||
return styles.normal;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
type SendouMenuItemProps,
|
||||
} from "../elements/Menu";
|
||||
import { PlusIcon } from "../icons/Plus";
|
||||
import styles from "./TopRightButtons.module.css";
|
||||
|
||||
export function AnythingAdder() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
|
@ -103,10 +104,10 @@ export function AnythingAdder() {
|
|||
<SendouMenu
|
||||
trigger={
|
||||
<Button
|
||||
className="layout__header__button"
|
||||
className={styles.button}
|
||||
data-testid="anything-adder-menu-button"
|
||||
>
|
||||
<PlusIcon className="layout__header__button__icon" />
|
||||
<PlusIcon className={styles.buttonIcon} />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
121
app/components/layout/Footer.module.css
Normal file
121
app/components/layout/Footer.module.css
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--s-2-5);
|
||||
background-color: var(--color-bg-high);
|
||||
gap: var(--s-6);
|
||||
margin-block-start: auto;
|
||||
padding-block-end: var(--s-32);
|
||||
}
|
||||
|
||||
.linkList {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.socialLink {
|
||||
display: flex;
|
||||
max-width: 10rem;
|
||||
height: 12rem;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-4);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-higher);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
height: 2.25rem;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.socialLink:hover > .socialIcon {
|
||||
transform: translateY(-0.3rem);
|
||||
}
|
||||
|
||||
.socialHeader {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.socialHeader > p {
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.patronTitle {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.patronList {
|
||||
display: flex;
|
||||
max-width: 75vw;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
font-size: var(--fonts-xs);
|
||||
gap: var(--s-1);
|
||||
list-style: none;
|
||||
margin-block-start: var(--s-2);
|
||||
}
|
||||
|
||||
.patron {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.copyrightNote {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.socials {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.socialLink {
|
||||
max-width: initial;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.socialHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.socialHeader > p {
|
||||
margin-block-start: var(--s-1);
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.socialLink:hover > .socialIcon {
|
||||
transform: translateX(-0.3rem);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { Image } from "../Image";
|
|||
import { DiscordIcon } from "../icons/Discord";
|
||||
import { GitHubIcon } from "../icons/GitHub";
|
||||
import { PatreonIcon } from "../icons/Patreon";
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -24,45 +25,45 @@ export function Footer() {
|
|||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="layout__footer">
|
||||
<div className="layout__footer__link-list">
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.linkList}>
|
||||
<Link to={PRIVACY_POLICY_PAGE}>{t("pages.privacy")}</Link>
|
||||
<Link to={CONTRIBUTIONS_PAGE}>{t("pages.contributors")}</Link>
|
||||
<Link to={FAQ_PAGE}>{t("pages.faq")}</Link>
|
||||
<Link to={API_PAGE}>{t("pages.api")}</Link>
|
||||
</div>
|
||||
<div className="layout__footer__socials">
|
||||
<div className={styles.socials}>
|
||||
<a
|
||||
className="layout__footer__social-link"
|
||||
className={styles.socialLink}
|
||||
href={SENDOU_INK_GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="layout__footer__social-header">
|
||||
<div className={styles.socialHeader}>
|
||||
GitHub<p>{t("footer.github.subtitle")}</p>
|
||||
</div>
|
||||
<GitHubIcon className="layout__footer__social-icon github" />
|
||||
<GitHubIcon className={styles.socialIcon} />
|
||||
</a>
|
||||
<a
|
||||
className="layout__footer__social-link"
|
||||
className={styles.socialLink}
|
||||
href={SENDOU_INK_DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="layout__footer__social-header">
|
||||
<div className={styles.socialHeader}>
|
||||
Discord<p>{t("footer.discord.subtitle")}</p>
|
||||
</div>{" "}
|
||||
<DiscordIcon className="layout__footer__social-icon discord" />
|
||||
<DiscordIcon className={styles.socialIcon} />
|
||||
</a>
|
||||
<Link className="layout__footer__social-link" to={SUPPORT_PAGE}>
|
||||
<div className="layout__footer__social-header">
|
||||
<Link className={styles.socialLink} to={SUPPORT_PAGE}>
|
||||
<div className={styles.socialHeader}>
|
||||
Patreon<p>{t("footer.patreon.subtitle")}</p>
|
||||
</div>{" "}
|
||||
<PatreonIcon className="layout__footer__social-icon patreon" />
|
||||
<PatreonIcon className={styles.socialIcon} />
|
||||
</Link>
|
||||
</div>
|
||||
<PatronsList />
|
||||
<div className="layout__copyright-note">
|
||||
<div className={styles.copyrightNote}>
|
||||
<p>
|
||||
sendou.ink © Copyright of Sendou and contributors 2019-{currentYear}.
|
||||
Original content & source code is licensed under the AGPL-3.0 license.
|
||||
|
|
@ -94,17 +95,14 @@ function PatronsList() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="layout__footer__patron-title">
|
||||
<h4 className={styles.patronTitle}>
|
||||
{t("footer.thanks")}
|
||||
<Image alt="" path={SENDOU_LOVE_EMOJI_PATH} width={24} height={24} />
|
||||
</h4>
|
||||
<ul className="layout__footer__patron-list">
|
||||
<ul className={styles.patronList}>
|
||||
{patrons?.map((patron) => (
|
||||
<li key={patron.id}>
|
||||
<Link
|
||||
to={userPage(patron)}
|
||||
className="layout__footer__patron-list__patron"
|
||||
>
|
||||
<Link to={userPage(patron)} className={styles.patron}>
|
||||
{patron.username}
|
||||
</Link>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { LOG_IN_URL, SENDOU_INK_DISCORD_URL } from "~/utils/urls";
|
||||
import styles from "./UserItem.module.css";
|
||||
|
||||
export function LogInButtonContainer({
|
||||
children,
|
||||
|
|
@ -32,7 +33,7 @@ export function LogInButtonContainer({
|
|||
: t("auth.errors.failed")
|
||||
}
|
||||
>
|
||||
<div className="stack md layout__user-item">
|
||||
<div className={`stack md ${styles.userItem}`}>
|
||||
{authError === "aborted" ? (
|
||||
t("auth.errors.discordPermissions")
|
||||
) : (
|
||||
|
|
|
|||
92
app/components/layout/NavDialog.module.css
Normal file
92
app/components/layout/NavDialog.module.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
.dialog {
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
border-radius: 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
margin-inline-start: auto;
|
||||
margin-block-end: var(--s-4);
|
||||
margin-inline-end: var(--s-4);
|
||||
}
|
||||
|
||||
.closeButton > svg {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.itemsContainer {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
gap: var(--s-4) var(--s-4);
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
animation: smooth-appear 0.75s ease-out forwards;
|
||||
margin-top: 27.5px;
|
||||
opacity: 0.25;
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
@keyframes smooth-appear {
|
||||
to {
|
||||
margin-top: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
font-weight: bold;
|
||||
gap: var(--s-1);
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.logInButton {
|
||||
display: grid;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
background-color: var(--color-bg-high);
|
||||
color: var(--color-text);
|
||||
place-items: center;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: grid;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-high);
|
||||
color: var(--color-text);
|
||||
place-items: center;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.imageContainer.round {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.imageContainer:hover {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.dialog {
|
||||
padding-block: var(--s-12) !important;
|
||||
padding-inline: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import { Image } from "../Image";
|
|||
import { CrossIcon } from "../icons/Cross";
|
||||
import { LogOutIcon } from "../icons/LogOut";
|
||||
import { LogInButtonContainer } from "./LogInButtonContainer";
|
||||
import styles from "./NavDialog.module.css";
|
||||
|
||||
export function NavDialog({
|
||||
isOpen,
|
||||
|
|
@ -27,7 +28,7 @@ export function NavDialog({
|
|||
|
||||
return (
|
||||
<SendouDialog
|
||||
className="layout__overlay-nav__dialog"
|
||||
className={styles.dialog}
|
||||
showHeading={false}
|
||||
aria-label="Site navigation"
|
||||
isFullScreen
|
||||
|
|
@ -35,21 +36,21 @@ export function NavDialog({
|
|||
<SendouButton
|
||||
icon={<CrossIcon />}
|
||||
variant="minimal-destructive"
|
||||
className="layout__overlay-nav__close-button"
|
||||
className={styles.closeButton}
|
||||
onPress={close}
|
||||
aria-label="Close navigation dialog"
|
||||
/>
|
||||
<div className="layout__overlay-nav__nav-items-container">
|
||||
<div className={styles.itemsContainer}>
|
||||
<LogInButton close={close} />
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
to={`/${item.url}`}
|
||||
className="layout__overlay-nav__nav-item"
|
||||
className={styles.navItem}
|
||||
key={item.name}
|
||||
prefetch={item.prefetch ? "render" : undefined}
|
||||
onClick={close}
|
||||
>
|
||||
<div className="layout__overlay-nav__nav-image-container">
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
path={navIconUrl(item.name)}
|
||||
height={48}
|
||||
|
|
@ -85,18 +86,14 @@ function LogInButton({ close }: { close: () => void }) {
|
|||
|
||||
if (user) {
|
||||
return (
|
||||
<Link
|
||||
to={userPage(user)}
|
||||
className="layout__overlay-nav__nav-item"
|
||||
onClick={close}
|
||||
>
|
||||
<div className="layout__overlay-nav__nav-image-container">
|
||||
<Link to={userPage(user)} className={styles.navItem} onClick={close}>
|
||||
<div className={styles.imageContainer}>
|
||||
<Avatar
|
||||
user={user}
|
||||
alt={t("common:header.loggedInAs", {
|
||||
userName: `${user.username}`,
|
||||
})}
|
||||
className="layout__overlay-nav__avatar"
|
||||
className={styles.avatar}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -106,10 +103,10 @@ function LogInButton({ close }: { close: () => void }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="layout__overlay-nav__nav-item">
|
||||
<div className={styles.navItem}>
|
||||
<LogInButtonContainer>
|
||||
<button
|
||||
className="layout__overlay-nav__log-in-button layout__overlay-nav__nav-image-container"
|
||||
className={`${styles.logInButton} ${styles.imageContainer}`}
|
||||
type="submit"
|
||||
>
|
||||
<Image path={navIconUrl("log_in")} height={48} width={48} alt="" />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { BellIcon } from "../icons/Bell";
|
|||
import { RefreshIcon } from "../icons/Refresh";
|
||||
|
||||
import styles from "./NotificationPopover.module.css";
|
||||
import headerStyles from "./TopRightButtons.module.css";
|
||||
|
||||
export type LoaderNotification = NonNullable<
|
||||
RootLoaderData["notifications"]
|
||||
|
|
@ -48,10 +49,10 @@ export function NotificationPopover() {
|
|||
<SendouPopover
|
||||
trigger={
|
||||
<Button
|
||||
className="layout__header__button"
|
||||
className={headerStyles.button}
|
||||
data-testid="notifications-button"
|
||||
>
|
||||
<BellIcon />
|
||||
<BellIcon className={headerStyles.buttonIcon} />
|
||||
</Button>
|
||||
}
|
||||
popoverClassName={clsx(styles.popoverContainer, {
|
||||
|
|
|
|||
33
app/components/layout/TopRightButtons.module.css
Normal file
33
app/components/layout/TopRightButtons.module.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.container {
|
||||
display: flex;
|
||||
gap: var(--s-3);
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--color-bg);
|
||||
width: var(--item-size);
|
||||
height: var(--item-size);
|
||||
padding: 0.25rem;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 50%;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:focus-visible {
|
||||
outline: 2px solid var(--color-text-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
width: 1.15rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.container > .userItem {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { HamburgerIcon } from "../icons/Hamburger";
|
|||
import { HeartIcon } from "../icons/Heart";
|
||||
import { AnythingAdder } from "./AnythingAdder";
|
||||
import { NotificationPopover } from "./NotificationPopover";
|
||||
import styles from "./TopRightButtons.module.css";
|
||||
import { UserItem } from "./UserItem";
|
||||
|
||||
export function TopRightButtons({
|
||||
|
|
@ -19,7 +20,7 @@ export function TopRightButtons({
|
|||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<div className="layout__header__right-container">
|
||||
<div className={styles.container}>
|
||||
{showSupport ? (
|
||||
<LinkButton
|
||||
to={SUPPORT_PAGE}
|
||||
|
|
@ -35,12 +36,12 @@ export function TopRightButtons({
|
|||
<button
|
||||
aria-label="Open navigation"
|
||||
onClick={openNavDialog}
|
||||
className="layout__header__button"
|
||||
className={styles.button}
|
||||
type="button"
|
||||
>
|
||||
<HamburgerIcon className="layout__header__button__icon" />
|
||||
<HamburgerIcon className={styles.buttonIcon} />
|
||||
</button>
|
||||
{!isErrored ? <UserItem /> : null}
|
||||
{!isErrored ? <UserItem className={styles.userItem} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
40
app/components/layout/UserItem.module.css
Normal file
40
app/components/layout/UserItem.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.userItem {
|
||||
outline-offset: 1px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.userItem:focus-visible {
|
||||
outline: 2px solid var(--color-text-accent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: var(--item-size);
|
||||
height: var(--item-size);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.logInButton {
|
||||
display: flex;
|
||||
height: var(--item-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid;
|
||||
border-color: var(--color-text-accent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
gap: var(--s-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logInButton > svg {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.logInButton:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { userPage } from "~/utils/urls";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { LogInIcon } from "../icons/LogIn";
|
||||
import { LogInButtonContainer } from "./LogInButtonContainer";
|
||||
import styles from "./UserItem.module.css";
|
||||
|
||||
export function UserItem() {
|
||||
export function UserItem({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const user = useUser();
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<Link to={userPage(user)} prefetch="intent" className="layout__user-item">
|
||||
<Link
|
||||
to={userPage(user)}
|
||||
prefetch="intent"
|
||||
className={clsx(styles.userItem, className)}
|
||||
>
|
||||
<Avatar
|
||||
user={user}
|
||||
alt={t("header.loggedInAs", {
|
||||
userName: `${user.username}`,
|
||||
})}
|
||||
className="layout__avatar"
|
||||
className={styles.avatar}
|
||||
size="sm"
|
||||
/>
|
||||
</Link>
|
||||
|
|
@ -27,7 +33,7 @@ export function UserItem() {
|
|||
|
||||
return (
|
||||
<LogInButtonContainer>
|
||||
<button type="submit" className="layout__log-in-button">
|
||||
<button type="submit" className={styles.logInButton}>
|
||||
<LogInIcon /> {t("header.login")}
|
||||
</button>
|
||||
</LogInButtonContainer>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,85 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1.5px solid var(--color-border);
|
||||
-webkit-backdrop-filter: var(--backdrop-filter);
|
||||
backdrop-filter: var(--backdrop-filter);
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
padding-block: var(--s-2);
|
||||
padding-inline: var(--s-4);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.itemSize {
|
||||
--item-size: var(--input-height-small);
|
||||
}
|
||||
|
||||
.breadcrumbContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
overflow: hidden;
|
||||
max-width: 350px;
|
||||
color: var(--color-text);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 9999px;
|
||||
height: var(--item-size);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--input-focus-ring);
|
||||
outline-offset: 1px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbImage {
|
||||
min-width: var(--item-size);
|
||||
}
|
||||
|
||||
.logo {
|
||||
overflow: initial;
|
||||
padding: 0 var(--s-1);
|
||||
}
|
||||
|
||||
.logo:focus-visible {
|
||||
outline: var(--input-focus-ring);
|
||||
outline-offset: 1px;
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.breadcrumbSeparator {
|
||||
font-size: 20px;
|
||||
opacity: 0.4;
|
||||
margin-inline-start: -1px;
|
||||
}
|
||||
|
||||
.textMobileHidden {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.hamburger.fab {
|
||||
display: grid;
|
||||
position: fixed;
|
||||
|
|
@ -22,3 +104,20 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (display-mode: standalone) {
|
||||
.header {
|
||||
align-items: flex-end;
|
||||
padding-top: calc(var(--s-2) + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.textMobileHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumbContainer > a {
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function Layout({
|
|||
!data?.user?.roles.includes("MINOR_SUPPORT") &&
|
||||
!location.pathname.includes("plans");
|
||||
return (
|
||||
<div className="layout__container">
|
||||
<div className={styles.container}>
|
||||
<NavDialog isOpen={navDialogOpen} close={() => setNavDialogOpen(false)} />
|
||||
{isFrontPage ? (
|
||||
<SendouButton
|
||||
|
|
@ -62,16 +62,16 @@ export function Layout({
|
|||
onPress={() => setNavDialogOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
<header className="layout__header layout__item_size">
|
||||
<div className="layout__breadcrumb-container">
|
||||
<Link to="/" className="layout__breadcrumb logo">
|
||||
<header className={clsx(styles.header, styles.itemSize)}>
|
||||
<div className={styles.breadcrumbContainer}>
|
||||
<Link to="/" className={clsx(styles.breadcrumb, styles.logo)}>
|
||||
sendou.ink
|
||||
</Link>
|
||||
{breadcrumbs.flatMap((breadcrumb) => {
|
||||
return [
|
||||
<span
|
||||
key={`${breadcrumb.href}-sep`}
|
||||
className="layout__breadcrumb-separator"
|
||||
className={styles.breadcrumbSeparator}
|
||||
>
|
||||
»
|
||||
</span>,
|
||||
|
|
@ -101,13 +101,13 @@ function BreadcrumbLink({ data }: { data: Breadcrumb }) {
|
|||
return (
|
||||
<Link
|
||||
to={data.href}
|
||||
className={clsx("layout__breadcrumb", {
|
||||
className={clsx(styles.breadcrumb, {
|
||||
"stack horizontal sm items-center": data.text,
|
||||
})}
|
||||
>
|
||||
{imageIsWithExtension ? (
|
||||
<img
|
||||
className={clsx("layout__breadcrumb__image", {
|
||||
className={clsx(styles.breadcrumbImage, {
|
||||
"rounded-full": data.rounded,
|
||||
})}
|
||||
alt=""
|
||||
|
|
@ -117,7 +117,7 @@ function BreadcrumbLink({ data }: { data: Breadcrumb }) {
|
|||
/>
|
||||
) : (
|
||||
<Image
|
||||
className={clsx("layout__breadcrumb__image", {
|
||||
className={clsx(styles.breadcrumbImage, {
|
||||
"rounded-full": data.rounded,
|
||||
})}
|
||||
alt=""
|
||||
|
|
@ -126,15 +126,13 @@ function BreadcrumbLink({ data }: { data: Breadcrumb }) {
|
|||
height={24}
|
||||
/>
|
||||
)}
|
||||
<span className="layout__breadcrumb__text-mobile-hidden">
|
||||
{data.text}
|
||||
</span>
|
||||
<span className={styles.textMobileHidden}>{data.text}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={data.href} className="layout__breadcrumb">
|
||||
<Link to={data.href} className={styles.breadcrumb}>
|
||||
{data.text}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.badges__container {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
padding-inline: var(--s-3);
|
||||
}
|
||||
|
||||
.badges__small-badges {
|
||||
.smallBadges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
margin-block-start: var(--s-1);
|
||||
}
|
||||
|
||||
.badges__nav-link.active {
|
||||
.navLink.active {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badges__general-info-texts {
|
||||
.generalInfoTexts {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-high);
|
||||
|
|
@ -30,24 +30,24 @@
|
|||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
||||
.badges__explanation {
|
||||
.explanation {
|
||||
color: var(--color-text-accent);
|
||||
font-weight: var(--semi-bold);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badges__managers {
|
||||
.managers {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badges__owners-container {
|
||||
.ownersContainer {
|
||||
height: 8rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.badges__owners {
|
||||
.owners {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.badges__owners > li {
|
||||
.owners > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -64,19 +64,19 @@
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
.badges__count {
|
||||
.count {
|
||||
color: var(--color-accent-high);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.badges-edit__users-list > li {
|
||||
.editUsersList > li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.badges-edit__users-list {
|
||||
.editUsersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
|
@ -86,35 +86,35 @@
|
|||
padding-block-end: var(--s-2-5);
|
||||
}
|
||||
|
||||
.badges-edit__number-input {
|
||||
.editNumberInput {
|
||||
max-width: 5rem;
|
||||
}
|
||||
|
||||
.badges-edit__cancel-button {
|
||||
.editCancelButton {
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.badges-edit__big-header {
|
||||
.editBigHeader {
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.badges-edit__small-header {
|
||||
.editSmallHeader {
|
||||
font-size: var(--fonts-md);
|
||||
}
|
||||
|
||||
.badges-edit__differences {
|
||||
.editDifferences {
|
||||
padding: 0;
|
||||
font-size: var(--fonts-xs);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.badges-edit__differences > li::before {
|
||||
.editDifferences > li::before {
|
||||
content: "-";
|
||||
padding-inline-end: 5px;
|
||||
}
|
||||
|
||||
.badges-search__input {
|
||||
.searchInput {
|
||||
height: 40px !important;
|
||||
margin: 0 auto;
|
||||
font-size: var(--fonts-lg);
|
||||
|
|
@ -8,6 +8,7 @@ import { TrashIcon } from "~/components/icons/Trash";
|
|||
import type { Tables } from "~/db/tables";
|
||||
import { useHasPermission, useHasRole } from "~/modules/permissions/hooks";
|
||||
import { action } from "../actions/badges.$id.edit.server";
|
||||
import styles from "../badges.module.css";
|
||||
import type { BadgeDetailsLoaderData } from "../loaders/badges.$id.server";
|
||||
import type { BadgeDetailsContext } from "./badges.$id";
|
||||
export { action };
|
||||
|
|
@ -54,7 +55,7 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
return (
|
||||
<div className="stack md mx-auto">
|
||||
<div className="stack sm">
|
||||
<h3 className="badges-edit__small-header">Managers</h3>
|
||||
<h3 className={styles.editSmallHeader}>Managers</h3>
|
||||
<UserSearch
|
||||
key={managers.map((m) => m.id).join("-")}
|
||||
label="Add new manager"
|
||||
|
|
@ -69,7 +70,7 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
setManagers([...managers, user]);
|
||||
}}
|
||||
/>
|
||||
<ul className="badges-edit__users-list">
|
||||
<ul className={styles.editUsersList}>
|
||||
{managers.map((manager) => (
|
||||
<li key={manager.id}>
|
||||
{manager.username}
|
||||
|
|
@ -121,7 +122,7 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
return (
|
||||
<div className="stack md mx-auto">
|
||||
<div className="stack sm">
|
||||
<h3 className="badges-edit__small-header">Owners</h3>
|
||||
<h3 className={styles.editSmallHeader}>Owners</h3>
|
||||
<UserSearch
|
||||
label="Add new owner"
|
||||
className="text-center mx-auto"
|
||||
|
|
@ -143,12 +144,12 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<ul className="badges-edit__users-list">
|
||||
<ul className={styles.editUsersList}>
|
||||
{owners.map((owner) => (
|
||||
<li key={owner.id}>
|
||||
{owner.username}
|
||||
<input
|
||||
className="badges-edit__number-input"
|
||||
className={styles.editNumberInput}
|
||||
type="number"
|
||||
value={owner.count}
|
||||
min={0}
|
||||
|
|
@ -167,7 +168,7 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
))}
|
||||
</ul>
|
||||
{ownerDifferences.length > 0 ? (
|
||||
<ul className="badges-edit__differences">
|
||||
<ul className={styles.editDifferences}>
|
||||
{ownerDifferences.map((o) => (
|
||||
<li key={o.id}>
|
||||
{o.type === "added" ? (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { LinkButton } from "~/components/elements/Button";
|
|||
import { useHasPermission, useHasRole } from "~/modules/permissions/hooks";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { userPage } from "~/utils/urls";
|
||||
import styles from "../badges.module.css";
|
||||
import { badgeExplanationText } from "../badges-utils";
|
||||
|
||||
import { loader } from "../loaders/badges.$id.server";
|
||||
|
|
@ -29,10 +30,10 @@ export default function BadgeDetailsPage() {
|
|||
<Outlet context={context} />
|
||||
<Badge badge={data.badge} isAnimated size={200} />
|
||||
<div>
|
||||
<div className="badges__explanation">
|
||||
<div className={styles.explanation}>
|
||||
{badgeExplanationText(t, data.badge)}
|
||||
</div>
|
||||
<div className="badges__managers">
|
||||
<div className={styles.managers}>
|
||||
<Trans
|
||||
i18nKey="managedBy"
|
||||
ns="badges"
|
||||
|
|
@ -65,12 +66,12 @@ export default function BadgeDetailsPage() {
|
|||
Edit
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<div className="badges__owners-container">
|
||||
<ul className="badges__owners">
|
||||
<div className={styles.ownersContainer}>
|
||||
<ul className={styles.owners}>
|
||||
{data.badge.owners.map((owner) => (
|
||||
<li key={owner.id}>
|
||||
<span
|
||||
className={clsx("badges__count", {
|
||||
className={clsx(styles.count, {
|
||||
invisible: owner.count <= 1,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { metaTags } from "../../../utils/remix";
|
|||
import { type BadgesLoaderData, loader } from "../loaders/badges.server";
|
||||
export { loader };
|
||||
|
||||
import "~/styles/badges.css";
|
||||
import styles from "../badges.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "badges",
|
||||
|
|
@ -61,10 +61,10 @@ export default function BadgesPageLayout() {
|
|||
|
||||
return (
|
||||
<Main>
|
||||
<div className="badges__container">
|
||||
<div className={styles.container}>
|
||||
<Outlet />
|
||||
<Input
|
||||
className="badges-search__input"
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon />}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
|
|
@ -72,10 +72,10 @@ export default function BadgesPageLayout() {
|
|||
{ownBadges.length > 0 ? (
|
||||
<div className="w-full">
|
||||
<Divider smallText>{t("badges:own.divider")}</Divider>
|
||||
<div className="badges__small-badges">
|
||||
<div className={styles.smallBadges}>
|
||||
{ownBadges.map((badge) => (
|
||||
<NavLink
|
||||
className="badges__nav-link"
|
||||
className={styles.navLink}
|
||||
key={badge.id}
|
||||
to={String(badge.id)}
|
||||
>
|
||||
|
|
@ -87,13 +87,13 @@ export default function BadgesPageLayout() {
|
|||
) : null}
|
||||
{ownBadges.length > 0 || otherBadges.length > 0 ? (
|
||||
<div className="w-full">
|
||||
<div className="badges__small-badges">
|
||||
<div className={styles.smallBadges}>
|
||||
{ownBadges.length > 0 ? (
|
||||
<Divider smallText>{t("badges:other.divider")}</Divider>
|
||||
) : null}
|
||||
{otherBadges.map((badge) => (
|
||||
<NavLink
|
||||
className="badges__nav-link"
|
||||
className={styles.navLink}
|
||||
key={badge.id}
|
||||
to={String(badge.id)}
|
||||
>
|
||||
|
|
@ -108,7 +108,7 @@ export default function BadgesPageLayout() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="badges__general-info-texts">
|
||||
<div className={styles.generalInfoTexts}>
|
||||
<p>
|
||||
<a href={BADGES_DOC_LINK} target="_blank" rel="noopener noreferrer">
|
||||
{t("forYourEvent")}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { MAX_AP } from "../analyzer-constants";
|
|||
import type { FullInkTankOption } from "../analyzer-types";
|
||||
import { fullInkTankOptions } from "../core/stats";
|
||||
import { weaponParams } from "../core/utils";
|
||||
import styles from "../routes/analyzer.module.css";
|
||||
|
||||
interface PerInkTankGridProps {
|
||||
weaponSplId: MainWeaponId;
|
||||
|
|
@ -19,7 +20,7 @@ export function PerInkTankGrid(props: PerInkTankGridProps) {
|
|||
|
||||
return (
|
||||
<SendouPopover
|
||||
popoverClassName="analyzer__ink-grid__container"
|
||||
popoverClassName={styles.inkGridContainer}
|
||||
trigger={
|
||||
<SendouButton variant="minimal" size="small">
|
||||
{t("analyzer:button.showConsumptionGrid")}
|
||||
|
|
@ -92,19 +93,20 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
|
|||
</div>
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration */}
|
||||
<div className="stack horizontal sm" onMouseLeave={handleMouseLeaveGrid}>
|
||||
<div className="analyzer__ink-grid__horizontal-ability">
|
||||
<div className={styles.inkGridHorizontalAbility}>
|
||||
<Ability ability="ISS" size="SUBTINY" />
|
||||
</div>
|
||||
<div className="analyzer__ink-grid">
|
||||
<div className="analyzer__ink-grid__horizontal-ability">
|
||||
<div className={styles.inkGrid}>
|
||||
<div className={styles.inkGridHorizontalAbility}>
|
||||
<Ability ability="ISM" size="SUBTINY" />
|
||||
</div>
|
||||
<div />
|
||||
{AP_VALUES_TO_SHOW.map((ap) => (
|
||||
<div
|
||||
className={clsx("analyzer__ink-grid__ap", {
|
||||
"analyzer__ink-grid__ap__focused": ismHovered === ap,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.inkGridAp,
|
||||
ismHovered === ap && styles.inkGridApFocused,
|
||||
)}
|
||||
key={ap}
|
||||
>
|
||||
{ap}
|
||||
|
|
@ -113,10 +115,11 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
|
|||
{values.map((row, i) =>
|
||||
[
|
||||
<div
|
||||
className={clsx("analyzer__ink-grid__ap", {
|
||||
"analyzer__ink-grid__ap__focused":
|
||||
issHovered === AP_VALUES_TO_SHOW[i],
|
||||
})}
|
||||
className={clsx(
|
||||
styles.inkGridAp,
|
||||
issHovered === AP_VALUES_TO_SHOW[i] &&
|
||||
styles.inkGridApFocused,
|
||||
)}
|
||||
key={i}
|
||||
>
|
||||
{AP_VALUES_TO_SHOW[i]}
|
||||
|
|
@ -126,7 +129,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
|
|||
const key = `${i}-${j}`;
|
||||
|
||||
if (cell === "N/A") {
|
||||
return <div className="analyzer__ink-grid__cell" key={key} />;
|
||||
return <div className={styles.inkGridCell} key={key} />;
|
||||
}
|
||||
|
||||
const title = `${cell.shots ?? "-"} (ISM: ${cell.ismAP}, ISS: ${
|
||||
|
|
@ -137,7 +140,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
|
|||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
|
||||
<div
|
||||
className="analyzer__ink-grid__cell"
|
||||
className={styles.inkGridCell}
|
||||
key={key}
|
||||
style={{ "--cell-color": "var(--color-bg-high)" }}
|
||||
title={title}
|
||||
|
|
@ -154,7 +157,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
|
|||
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
|
||||
<div
|
||||
key={key}
|
||||
className="analyzer__ink-grid__cell"
|
||||
className={styles.inkGridCell}
|
||||
style={{ "--cell-color": cell.hex }}
|
||||
title={title}
|
||||
onMouseEnter={() =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
.analyzer__container {
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--s-10);
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
.analyzer__left-column {
|
||||
.leftColumn {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
display: flex;
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.analyzer__ap-compare {
|
||||
.apCompare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content max-content max-content 1fr;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.analyzer__ap-compare__mains {
|
||||
.apCompareMains {
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -30,27 +30,27 @@
|
|||
margin-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ap-compare__bar {
|
||||
.apCompareBar {
|
||||
height: 100%;
|
||||
background-color: var(--color-info);
|
||||
}
|
||||
|
||||
.analyzer__ap-compare__bar.analyzer__better {
|
||||
.apCompareBar.better {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.analyzer__effects-selector {
|
||||
.effectsSelector {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: 1fr 2.5fr;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.analyzer__lde-intensity-select {
|
||||
.ldeIntensitySelect {
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.analyzer__ap-summary {
|
||||
.apSummary {
|
||||
width: 100%;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-high);
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__noticeable-link {
|
||||
.noticeableLink {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
|
@ -73,23 +73,23 @@
|
|||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__stat-popover-trigger {
|
||||
.statPopoverTrigger {
|
||||
border-radius: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.analyzer__stat-popover-trigger__icon {
|
||||
.statPopoverTriggerIcon {
|
||||
width: 16px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.analyzer__ap-text {
|
||||
.apText {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.analyzer__summary {
|
||||
.summary {
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-high);
|
||||
font-size: var(--fonts-md);
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.analyzer__details:has(.analyzer__stat-card-highlighted) .analyzer__summary {
|
||||
.details:has(.statCardHighlighted) .summary {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.analyzer__weapon-info-badge {
|
||||
.weaponInfoBadge {
|
||||
display: inline-flex;
|
||||
font-size: var(--fonts-xs);
|
||||
gap: var(--s-2);
|
||||
|
|
@ -112,15 +112,12 @@
|
|||
border-radius: var(--rounded);
|
||||
padding: var(--s-1) var(--s-3);
|
||||
margin-inline-start: auto;
|
||||
/** Decided to do it absolutely because
|
||||
summary really dislikes becoming a flex
|
||||
(arrow disappears) */
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 6.5px;
|
||||
}
|
||||
|
||||
.analyzer__weapon-info-badge__text {
|
||||
.weaponInfoBadgeText {
|
||||
max-width: 140px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -128,19 +125,19 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.analyzer__weapon-info-badge__text {
|
||||
.weaponInfoBadgeText {
|
||||
max-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.analyzer__stat-collection {
|
||||
.statCollection {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
|
||||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__stat-card {
|
||||
.statCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
|
@ -150,11 +147,11 @@
|
|||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__stat-card-highlighted {
|
||||
.statCardHighlighted {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.analyzer__stat-card__title-and-value-container {
|
||||
.statCardTitleAndValueContainer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
|
@ -162,21 +159,21 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__title {
|
||||
.statCardTitle {
|
||||
font-size: var(--fonts-xs);
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__ability-container {
|
||||
.statCardAbilityContainer {
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__popover-trigger {
|
||||
.statCardPopoverTrigger {
|
||||
display: inline;
|
||||
height: 1rem;
|
||||
padding: 0;
|
||||
|
|
@ -188,21 +185,21 @@
|
|||
outline: initial;
|
||||
}
|
||||
|
||||
.analyzer__stat-card-values {
|
||||
.statCardValues {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-top: var(--s-2);
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value {
|
||||
.statCardValue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value__title {
|
||||
.statCardValueTitle {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: 400;
|
||||
|
|
@ -210,12 +207,12 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value__number {
|
||||
.statCardValueNumber {
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.analyzer__table-container {
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
padding: var(--s-3);
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -224,29 +221,29 @@
|
|||
padding-block: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__shots-to-splat {
|
||||
.shotsToSplat {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxxs);
|
||||
margin-inline-start: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__consumption-table-explanation {
|
||||
.consumptionTableExplanation {
|
||||
margin-top: var(--s-2);
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.analyzer__stat-category-explanation {
|
||||
.statCategoryExplanation {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
margin-block-start: var(--s-3);
|
||||
}
|
||||
|
||||
.analyzer__sub-nav {
|
||||
.subNav {
|
||||
margin-block-end: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__patch {
|
||||
.patch {
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-accent-low);
|
||||
color: var(--color-accent-high);
|
||||
|
|
@ -257,16 +254,16 @@
|
|||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.analyzer__container {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.analyzer__left-column {
|
||||
.leftColumn {
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.analyzer__stat-popover {
|
||||
.statPopover {
|
||||
min-width: 360px;
|
||||
|
||||
--chart-bg: transparent;
|
||||
|
|
@ -275,51 +272,51 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 700px) {
|
||||
.analyzer__stat-popover {
|
||||
.statPopover {
|
||||
min-width: calc(360px * 1.75);
|
||||
--chart-height: calc(250px * 1.75);
|
||||
--chart-width: calc(340px * 1.75);
|
||||
}
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__container {
|
||||
.inkGridContainer {
|
||||
overflow: auto;
|
||||
min-width: min(100vw, 1100px);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid {
|
||||
.inkGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(41, 1fr);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__vertical-ability {
|
||||
.inkGridVerticalAbility {
|
||||
grid-row: span 41;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding-inline-end: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__horizontal-ability {
|
||||
.inkGridHorizontalAbility {
|
||||
grid-column: span 41;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__ap {
|
||||
.inkGridAp {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: var(--semi-bold);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__ap__focused {
|
||||
.inkGridApFocused {
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--bold);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__cell {
|
||||
.inkGridCell {
|
||||
width: 25px;
|
||||
font-size: var(--fonts-xs);
|
||||
display: grid;
|
||||
|
|
@ -4,9 +4,11 @@ import { Link } from "@remix-run/react";
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as R from "remeda";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import Chart from "~/components/Chart";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import {
|
||||
SendouTab,
|
||||
SendouTabList,
|
||||
|
|
@ -16,7 +18,9 @@ import {
|
|||
import { Image } from "~/components/Image";
|
||||
import { BeakerIcon } from "~/components/icons/Beaker";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { Table } from "~/components/Table";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { abilitiesShort } from "~/modules/in-game-lists/abilities";
|
||||
|
|
@ -39,6 +43,7 @@ import {
|
|||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
ANALYZER_URL,
|
||||
|
|
@ -83,12 +88,7 @@ import {
|
|||
isMainOnlyAbility,
|
||||
isStackableAbility,
|
||||
} from "../core/utils";
|
||||
import "../analyzer.css";
|
||||
import * as R from "remeda";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { Placeholder } from "~/components/Placeholder";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { logger } from "~/utils/logger";
|
||||
import styles from "./analyzer.module.css";
|
||||
|
||||
export const CURRENT_PATCH = "10.1";
|
||||
|
||||
|
|
@ -244,8 +244,8 @@ function BuildAnalyzerPage() {
|
|||
|
||||
return (
|
||||
<Main>
|
||||
<div className="analyzer__container">
|
||||
<div className="analyzer__left-column">
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className="stack sm items-center w-full">
|
||||
<div className="w-full">
|
||||
<WeaponSelect
|
||||
|
|
@ -272,7 +272,7 @@ function BuildAnalyzerPage() {
|
|||
handleChange({ newFocused: 3 });
|
||||
}
|
||||
}}
|
||||
className="analyzer__sub-nav"
|
||||
className={styles.subNav}
|
||||
>
|
||||
<SendouTabList>
|
||||
<SendouTab id="build-1" data-testid="build1-tab">
|
||||
|
|
@ -359,7 +359,7 @@ function BuildAnalyzerPage() {
|
|||
<AbilityChunksRequired build={build} />
|
||||
)}
|
||||
</div>
|
||||
<div className="analyzer__patch">
|
||||
<div className={styles.patch}>
|
||||
{t("analyzer:patch")} {CURRENT_PATCH}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -368,14 +368,14 @@ function BuildAnalyzerPage() {
|
|||
<StatCategory
|
||||
title={t("analyzer:stat.category.main")}
|
||||
summaryRightContent={
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
<div className={styles.weaponInfoBadge}>
|
||||
<Image
|
||||
path={mainWeaponImageUrl(mainWeaponId)}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={t(`weapons:MAIN_${mainWeaponId}`)}
|
||||
/>
|
||||
<span className="analyzer__weapon-info-badge__text">
|
||||
<span className={styles.weaponInfoBadgeText}>
|
||||
{t(`weapons:MAIN_${mainWeaponId}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -388,7 +388,7 @@ function BuildAnalyzerPage() {
|
|||
<StatCategory
|
||||
title={t("analyzer:stat.category.sub")}
|
||||
summaryRightContent={
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
<div className={styles.weaponInfoBadge}>
|
||||
<Image
|
||||
path={subWeaponImageUrl(analyzed.weapon.subWeaponSplId)}
|
||||
width={20}
|
||||
|
|
@ -477,7 +477,7 @@ function BuildAnalyzerPage() {
|
|||
<StatCategory
|
||||
title={t("analyzer:stat.category.special")}
|
||||
summaryRightContent={
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
<div className={styles.weaponInfoBadge}>
|
||||
<Image
|
||||
path={specialWeaponImageUrl(
|
||||
analyzed.weapon.specialWeaponSplId,
|
||||
|
|
@ -742,13 +742,13 @@ function BuildAnalyzerPage() {
|
|||
{analyzed.stats.subWeaponDefenseDamages.length > 0 && (
|
||||
<StatCategory
|
||||
title={t("analyzer:stat.category.subWeaponDefenseDamages")}
|
||||
containerClassName="analyzer__table-container"
|
||||
containerClassName={styles.tableContainer}
|
||||
textBelow={t("analyzer:damageSubDefExplanation")}
|
||||
>
|
||||
{(["SRU"] as const).some(
|
||||
(ability) => (abilityPoints.get(ability) ?? 0) > 0,
|
||||
) ? (
|
||||
<div className="analyzer__stat-card-highlighted" />
|
||||
<div className={styles.statCardHighlighted} />
|
||||
) : null}
|
||||
<DamageTable
|
||||
showPopovers
|
||||
|
|
@ -771,7 +771,7 @@ function BuildAnalyzerPage() {
|
|||
{analyzed.stats.damages.length > 0 && (
|
||||
<StatCategory
|
||||
title={t("analyzer:stat.category.damage")}
|
||||
containerClassName="analyzer__table-container"
|
||||
containerClassName={styles.tableContainer}
|
||||
>
|
||||
<DamageTable
|
||||
values={analyzed.stats.damages}
|
||||
|
|
@ -787,7 +787,7 @@ function BuildAnalyzerPage() {
|
|||
`weapons:SPECIAL_${analyzed.weapon.specialWeaponSplId}`,
|
||||
),
|
||||
})}
|
||||
containerClassName="analyzer__table-container"
|
||||
containerClassName={styles.tableContainer}
|
||||
>
|
||||
<DamageTable values={analyzed.stats.specialWeaponDamages} />
|
||||
</StatCategory>
|
||||
|
|
@ -796,12 +796,12 @@ function BuildAnalyzerPage() {
|
|||
{analyzed.stats.fullInkTankOptions.length > 0 && (
|
||||
<StatCategory
|
||||
title={t("analyzer:stat.category.actionsPerInkTank")}
|
||||
containerClassName="analyzer__table-container"
|
||||
containerClassName={styles.tableContainer}
|
||||
>
|
||||
{(["ISM", "ISS"] as const).some(
|
||||
(ability) => (abilityPoints.get(ability) ?? 0) > 0,
|
||||
) ? (
|
||||
<div className="analyzer__stat-card-highlighted" />
|
||||
<div className={styles.statCardHighlighted} />
|
||||
) : null}
|
||||
<ConsumptionTable
|
||||
isComparing={context.isComparing}
|
||||
|
|
@ -938,7 +938,7 @@ function BuildAnalyzerPage() {
|
|||
</StatCategory>
|
||||
{objectShredderSelected && (
|
||||
<Link
|
||||
className="analyzer__noticeable-link"
|
||||
className={styles.noticeableLink}
|
||||
to={objectDamageCalculatorPage(mainWeaponId)}
|
||||
>
|
||||
<Image
|
||||
|
|
@ -952,7 +952,7 @@ function BuildAnalyzerPage() {
|
|||
)}
|
||||
{user && focusedBuild && !buildIsEmpty(focusedBuild) ? (
|
||||
<Link
|
||||
className="analyzer__noticeable-link"
|
||||
className={styles.noticeableLink}
|
||||
to={userNewBuildPage(user, {
|
||||
weapon: mainWeaponId,
|
||||
build: focusedBuild,
|
||||
|
|
@ -989,17 +989,15 @@ function StatChartPopover(props: StatChartProps) {
|
|||
|
||||
return (
|
||||
<SendouPopover
|
||||
popoverClassName="analyzer__stat-popover"
|
||||
popoverClassName={styles.statPopover}
|
||||
trigger={
|
||||
<SendouButton
|
||||
className={
|
||||
props.simple ? undefined : "analyzer__stat-popover-trigger"
|
||||
}
|
||||
className={props.simple ? undefined : styles.statPopoverTrigger}
|
||||
variant="minimal"
|
||||
size="small"
|
||||
icon={
|
||||
<BeakerIcon
|
||||
className="analyzer__stat-popover-trigger__icon"
|
||||
className={styles.statPopoverTriggerIcon}
|
||||
title={t("analyzer:button.showChart")}
|
||||
/>
|
||||
}
|
||||
|
|
@ -1191,16 +1189,16 @@ function APCompare({
|
|||
buildMains.length > 0 || build2Mains.length > 0;
|
||||
|
||||
return (
|
||||
<div className="analyzer__ap-compare">
|
||||
<div className={styles.apCompare}>
|
||||
{hasAtLeastOneMainOnlyAbility ? (
|
||||
<>
|
||||
<div className="analyzer__ap-compare__mains">
|
||||
<div className={styles.apCompareMains}>
|
||||
{buildMains.map((ability) => (
|
||||
<Ability key={ability} ability={ability} size="TINY" />
|
||||
))}
|
||||
</div>
|
||||
<div />
|
||||
<div className="analyzer__ap-compare__mains">
|
||||
<div className={styles.apCompareMains}>
|
||||
{build2Mains.map((ability) => (
|
||||
<Ability key={ability} ability={ability} size="TINY" />
|
||||
))}
|
||||
|
|
@ -1225,16 +1223,16 @@ function APCompare({
|
|||
{t("analyzer:abilityPoints.short")}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("analyzer__ap-compare__bar", "justify-self-end", {
|
||||
analyzer__better: ap >= ap2,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.apCompareBar,
|
||||
"justify-self-end",
|
||||
ap >= ap2 && styles.better,
|
||||
)}
|
||||
style={{ width: `${ap}px` }}
|
||||
/>
|
||||
<Ability ability={ability} size="TINY" />
|
||||
<div
|
||||
className={clsx("analyzer__ap-compare__bar", {
|
||||
analyzer__better: ap <= ap2,
|
||||
})}
|
||||
className={clsx(styles.apCompareBar, ap <= ap2 && styles.better)}
|
||||
style={{ width: `${ap2}px` }}
|
||||
/>
|
||||
<div
|
||||
|
|
@ -1278,7 +1276,7 @@ function EffectsSelector({
|
|||
).reverse(); // reverse to show Tacticooler first as it always shows
|
||||
|
||||
return (
|
||||
<div className="analyzer__effects-selector">
|
||||
<div className={styles.effectsSelector}>
|
||||
{effectsToShow.map((effect) => {
|
||||
return (
|
||||
<React.Fragment key={effect.type}>
|
||||
|
|
@ -1301,7 +1299,7 @@ function EffectsSelector({
|
|||
onChange={(e) =>
|
||||
handleLdeIntensityChange(Number(e.target.value))
|
||||
}
|
||||
className="analyzer__lde-intensity-select"
|
||||
className={styles.ldeIntensitySelect}
|
||||
>
|
||||
{new Array(MAX_LDE_INTENSITY + 1).fill(null).map((_, i) => {
|
||||
const percentage = ((i / MAX_LDE_INTENSITY) * 100)
|
||||
|
|
@ -1345,7 +1343,7 @@ function AbilityChunksRequired({
|
|||
|
||||
return (
|
||||
<details className="w-full">
|
||||
<summary className="analyzer__ap-summary">{t("abilityChunks")}</summary>
|
||||
<summary className={styles.apSummary}>{t("abilityChunks")}</summary>
|
||||
<div className="stack sm horizontal flex-wrap mt-4">
|
||||
{abilityChunksMapAsArray.map((a) => {
|
||||
const mainAbilityName = a[0];
|
||||
|
|
@ -1357,7 +1355,7 @@ function AbilityChunksRequired({
|
|||
className="stack items-center"
|
||||
>
|
||||
<Ability ability={mainAbilityName} size="TINY" />
|
||||
<div className="analyzer__ap-text">{numChunksRequired}</div>
|
||||
<div className={styles.apText}>{numChunksRequired}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1369,7 +1367,7 @@ function AbilityChunksRequired({
|
|||
function StatCategory({
|
||||
title,
|
||||
children,
|
||||
containerClassName = "analyzer__stat-collection",
|
||||
containerClassName = styles.statCollection,
|
||||
textBelow,
|
||||
summaryRightContent,
|
||||
testId,
|
||||
|
|
@ -1382,14 +1380,14 @@ function StatCategory({
|
|||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<details className="analyzer__details">
|
||||
<summary className="analyzer__summary" data-testid={testId}>
|
||||
<details className={styles.details}>
|
||||
<summary className={styles.summary} data-testid={testId}>
|
||||
{title}
|
||||
{summaryRightContent}
|
||||
</summary>
|
||||
<div className={containerClassName}>{children}</div>
|
||||
{textBelow && (
|
||||
<div className="analyzer__stat-category-explanation">{textBelow}</div>
|
||||
<div className={styles.statCategoryExplanation}>{textBelow}</div>
|
||||
)}
|
||||
</details>
|
||||
);
|
||||
|
|
@ -1454,18 +1452,19 @@ function StatCard({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx("analyzer__stat-card", {
|
||||
"analyzer__stat-card-highlighted": isHighlighted(),
|
||||
})}
|
||||
className={clsx(
|
||||
styles.statCard,
|
||||
isHighlighted() && styles.statCardHighlighted,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className="analyzer__stat-card__title-and-value-container">
|
||||
<h3 className="analyzer__stat-card__title">
|
||||
<div className={styles.statCardTitleAndValueContainer}>
|
||||
<h3 className={styles.statCardTitle}>
|
||||
{title}{" "}
|
||||
{popoverInfo && (
|
||||
<SendouPopover
|
||||
trigger={
|
||||
<SendouButton className="analyzer__stat-card__popover-trigger">
|
||||
<SendouButton className={styles.statCardPopoverTrigger}>
|
||||
?
|
||||
</SendouButton>
|
||||
}
|
||||
|
|
@ -1474,9 +1473,9 @@ function StatCard({
|
|||
</SendouPopover>
|
||||
)}
|
||||
</h3>
|
||||
<div className="analyzer__stat-card-values">
|
||||
<div className="analyzer__stat-card__value">
|
||||
<h4 className="analyzer__stat-card__value__title">
|
||||
<div className={styles.statCardValues}>
|
||||
<div className={styles.statCardValue}>
|
||||
<h4 className={styles.statCardValueTitle}>
|
||||
{typeof stat === "number"
|
||||
? t("value")
|
||||
: showComparison
|
||||
|
|
@ -1484,7 +1483,7 @@ function StatCard({
|
|||
: t("base")}
|
||||
</h4>{" "}
|
||||
<div
|
||||
className="analyzer__stat-card__value__number"
|
||||
className={styles.statCardValueNumber}
|
||||
data-testid={testId ? `${testId}-base` : undefined}
|
||||
>
|
||||
{showComparison ? (stat as StatTuple)[0].value : baseValue}
|
||||
|
|
@ -1492,14 +1491,14 @@ function StatCard({
|
|||
</div>
|
||||
</div>
|
||||
{showBuildValue() ? (
|
||||
<div className="analyzer__stat-card__value">
|
||||
<div className={styles.statCardValue}>
|
||||
<h4
|
||||
className="analyzer__stat-card__value__title"
|
||||
className={styles.statCardValueTitle}
|
||||
data-testid={testId ? `${testId}-build-title` : undefined}
|
||||
>
|
||||
{showComparison ? t("build2") : t("build")}
|
||||
</h4>{" "}
|
||||
<div className="analyzer__stat-card__value__number">
|
||||
<div className={styles.statCardValueNumber}>
|
||||
{(stat as StatTuple)[showComparison ? 1 : 0].value}
|
||||
{suffix}
|
||||
</div>
|
||||
|
|
@ -1508,7 +1507,7 @@ function StatCard({
|
|||
</div>
|
||||
</div>
|
||||
{/* always render this so it reserves space */}
|
||||
<div className="analyzer__stat-card__ability-container">
|
||||
<div className={styles.statCardAbilityContainer}>
|
||||
{!isStaticValue && (
|
||||
<>
|
||||
<ModifiedByAbilities abilities={stat[0].modifiedBy} />
|
||||
|
|
@ -1659,7 +1658,7 @@ function DamageTable({
|
|||
{damage(val)}
|
||||
{comparisonVal ? `/${damage(comparisonVal)}` : null}{" "}
|
||||
{val.shotsToSplat && (
|
||||
<span className="analyzer__shots-to-splat">
|
||||
<span className={styles.shotsToSplat}>
|
||||
{t("analyzer:damage.toSplat", {
|
||||
count: val.shotsToSplat,
|
||||
})}
|
||||
|
|
@ -1776,7 +1775,7 @@ function ConsumptionTable({
|
|||
</tbody>
|
||||
</Table>
|
||||
{subWeaponId === TORPEDO_ID && (
|
||||
<div className="analyzer__consumption-table-explanation">
|
||||
<div className={styles.consumptionTableExplanation}>
|
||||
{t("analyzer:torpedoExplanation")}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
.build-stats__ability-row {
|
||||
.abilityRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.build-stats__bars {
|
||||
.bars {
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
display: grid;
|
||||
|
|
@ -14,11 +14,7 @@
|
|||
row-gap: var(--s-1);
|
||||
}
|
||||
|
||||
.build-stats__bar {
|
||||
.bar {
|
||||
background-color: var(--color-accent);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.build-stats__value-text {
|
||||
width: 100px;
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ import { metaTags } from "../../../utils/remix";
|
|||
import { loader } from "../loaders/builds.$slug.stats.server";
|
||||
export { loader };
|
||||
|
||||
import "../build-stats.css";
|
||||
import { MAX_AP } from "~/features/build-analyzer/analyzer-constants";
|
||||
import styles from "./builds.$slug.stats.module.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
|
@ -79,11 +79,11 @@ export default function BuildStatsPage() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div key={stats.name} className="build-stats__ability-row">
|
||||
<div key={stats.name} className={styles.abilityRow}>
|
||||
<div>
|
||||
<Ability ability={stats.name} size="SUB" />
|
||||
</div>
|
||||
<div className="build-stats__bars">
|
||||
<div className={styles.bars}>
|
||||
<div>
|
||||
<WeaponImage
|
||||
variant="badge"
|
||||
|
|
@ -95,7 +95,7 @@ export default function BuildStatsPage() {
|
|||
{stats.apAverage.weapon} {t("analyzer:abilityPoints.short")}
|
||||
</div>{" "}
|
||||
<div
|
||||
className="build-stats__bar"
|
||||
className={styles.bar}
|
||||
style={{ width: `${apToPx(stats.apAverage.weapon)}px` }}
|
||||
/>
|
||||
<div className="text-xs text-lighter font-bold justify-self-center">
|
||||
|
|
@ -105,7 +105,7 @@ export default function BuildStatsPage() {
|
|||
{stats.apAverage.all} {t("analyzer:abilityPoints.short")}
|
||||
</div>{" "}
|
||||
<div
|
||||
className="build-stats__bar"
|
||||
className={styles.bar}
|
||||
style={{ width: `${apToPx(stats.apAverage.all)}px` }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -123,9 +123,9 @@ export default function BuildStatsPage() {
|
|||
Math.floor((ap / MAX_AP) * 125);
|
||||
|
||||
return (
|
||||
<div key={stats.name} className="build-stats__ability-row">
|
||||
<div key={stats.name} className={styles.abilityRow}>
|
||||
<Ability ability={stats.name} size="SUB" />
|
||||
<div className="build-stats__bars">
|
||||
<div className={styles.bars}>
|
||||
<div>
|
||||
<WeaponImage
|
||||
variant="badge"
|
||||
|
|
@ -135,7 +135,7 @@ export default function BuildStatsPage() {
|
|||
</div>
|
||||
<div>{stats.percentage.weapon}%</div>{" "}
|
||||
<div
|
||||
className="build-stats__bar"
|
||||
className={styles.bar}
|
||||
style={{
|
||||
width: `${percentageToPx(stats.percentage.weapon)}px`,
|
||||
}}
|
||||
|
|
@ -145,7 +145,7 @@ export default function BuildStatsPage() {
|
|||
</div>
|
||||
<div>{stats.percentage.all}%</div>{" "}
|
||||
<div
|
||||
className="build-stats__bar"
|
||||
className={styles.bar}
|
||||
style={{
|
||||
width: `${percentageToPx(stats.percentage.all)}px`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
.event__title {
|
||||
.title {
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.event__day {
|
||||
.day {
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.event__times {
|
||||
.times {
|
||||
display: grid;
|
||||
column-gap: var(--s-1-5);
|
||||
font-weight: var(--semi-bold);
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
|
||||
.event__times > time {
|
||||
.times > time {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.event__map-pool-section {
|
||||
.mapPoolSection {
|
||||
display: flex;
|
||||
max-width: 32rem;
|
||||
flex-direction: column;
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.event__create-map-list-link {
|
||||
.createMapListLink {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
align-items: center;
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.event__author {
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-high);
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.event__results-players {
|
||||
.resultsPlayers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
|
|
@ -51,14 +51,14 @@
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
.event__results-section {
|
||||
.resultsSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.event__results-participant-count {
|
||||
.resultsParticipantCount {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
.calendar-new__container {
|
||||
.container {
|
||||
max-width: 38rem;
|
||||
}
|
||||
|
||||
.calendar-new__select {
|
||||
.select {
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.calendar-new__badges {
|
||||
.badges {
|
||||
width: max-content;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -15,15 +15,15 @@
|
|||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.calendar-new__day-label {
|
||||
.dayLabel {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.calendar-new__range-input {
|
||||
.rangeInput {
|
||||
width: 4.25rem;
|
||||
}
|
||||
|
||||
.calendar-new__avatar-preview {
|
||||
.avatarPreview {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
border-radius: 100%;
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import { z } from "zod/v4";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import "~/styles/calendar-new.css";
|
||||
import {
|
||||
bracketProgressionSchema,
|
||||
calendarEventTagSchema,
|
||||
} from "~/features/calendar/calendar-schemas";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import {
|
||||
actualNumber,
|
||||
checkboxValueToBoolean,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { type CalendarEventTag, TOURNAMENT_STAGE_TYPES } from "~/db/tables";
|
|||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import * as Swiss from "~/features/tournament-bracket/core/Swiss";
|
||||
import "~/styles/calendar-new.css";
|
||||
import { gamesShort, versusShort } from "~/modules/in-game-lists/games";
|
||||
import { modesShortWithSpecial } from "~/modules/in-game-lists/modes";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { action } from "../actions/calendar.$id.server";
|
||||
import styles from "../calendar-event.module.css";
|
||||
import {
|
||||
canDeleteCalendarEvent,
|
||||
canEditCalendarEvent,
|
||||
|
|
@ -40,8 +41,6 @@ import { Tags } from "../components/Tags";
|
|||
import { loader } from "../loaders/calendar.$id.server";
|
||||
export { loader, action };
|
||||
|
||||
import "~/styles/calendar-event.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader>;
|
||||
|
||||
|
|
@ -88,11 +87,11 @@ export default function CalendarEventPage() {
|
|||
return (
|
||||
<Main className="stack lg">
|
||||
<section className="stack sm">
|
||||
<div className="event__times">
|
||||
<div className={styles.times}>
|
||||
{data.event.startTimes.map((startTime, i) => (
|
||||
<React.Fragment key={startTime}>
|
||||
<span
|
||||
className={clsx("event__day", {
|
||||
className={clsx(styles.day, {
|
||||
hidden: data.event.startTimes.length === 1,
|
||||
})}
|
||||
>
|
||||
|
|
@ -202,9 +201,9 @@ function Results() {
|
|||
);
|
||||
|
||||
return (
|
||||
<Section title={t("calendar:results")} className="event__results-section">
|
||||
<Section title={t("calendar:results")} className={styles.resultsSection}>
|
||||
{data.event.participantCount && (
|
||||
<div className="event__results-participant-count">
|
||||
<div className={styles.resultsParticipantCount}>
|
||||
{isTeamResults
|
||||
? t("calendar:participatedCount", {
|
||||
count: data.event.participantCount,
|
||||
|
|
@ -230,7 +229,7 @@ function Results() {
|
|||
</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td>
|
||||
<ul className="event__results-players">
|
||||
<ul className={styles.resultsPlayers}>
|
||||
{result.players.map((player) => {
|
||||
return (
|
||||
<li
|
||||
|
|
@ -272,10 +271,10 @@ function MapPoolInfo() {
|
|||
|
||||
return (
|
||||
<Section title={t("calendar:forms.mapPool")}>
|
||||
<div className="event__map-pool-section">
|
||||
<div className={styles.mapPoolSection}>
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
<LinkButton
|
||||
className="event__create-map-list-link"
|
||||
className={styles.createMapListLink}
|
||||
to={mapsPageWithMapPool(mapPool)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
|
|
@ -295,7 +294,7 @@ function Description() {
|
|||
return (
|
||||
<Section title={t("forms.description")}>
|
||||
<div className="stack sm">
|
||||
<div className="event__author">
|
||||
<div className={styles.author}>
|
||||
<Avatar user={data.event} size="xs" />
|
||||
{data.event.username}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Badge } from "~/components/Badge";
|
|||
import { DateInput } from "~/components/DateInput";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { Input } from "~/components/Input";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
|
|
@ -25,20 +26,25 @@ import * as Progression from "~/features/tournament-bracket/core/Progression";
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import type { RankedModeShort } from "~/modules/in-game-lists/types";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
getDateAtNextFullHour,
|
||||
getDateWithHoursOffset,
|
||||
} from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { pathnameFromPotentialURL } from "~/utils/strings";
|
||||
import { CREATING_TOURNAMENT_DOC_LINK, FAQ_PAGE } from "~/utils/urls";
|
||||
import { action } from "../actions/calendar.new.server";
|
||||
import {
|
||||
CALENDAR_EVENT,
|
||||
REG_CLOSES_AT_OPTIONS,
|
||||
type RegClosesAtOption,
|
||||
} from "../calendar-constants";
|
||||
import styles from "../calendar-new.module.css";
|
||||
import {
|
||||
calendarEventMaxDate,
|
||||
calendarEventMinDate,
|
||||
|
|
@ -47,12 +53,6 @@ import {
|
|||
} from "../calendar-utils";
|
||||
import { BracketProgressionSelector } from "../components/BracketProgressionSelector";
|
||||
import { Tags } from "../components/Tags";
|
||||
import "~/styles/calendar-new.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { action } from "../actions/calendar.new.server";
|
||||
import { loader } from "../loaders/calendar.new.server";
|
||||
export { loader, action };
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ export default function CalendarNewEventPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Main className="calendar-new__container">
|
||||
<Main className={styles.container}>
|
||||
<div className="stack md">
|
||||
<div className="stack horizontal md items-center">
|
||||
<h1 className="text-lg">
|
||||
|
|
@ -494,7 +494,7 @@ function DatesInput({ allowMultiDate }: { allowMultiDate?: boolean }) {
|
|||
<div key={key} className="stack horizontal sm items-center">
|
||||
<label
|
||||
id={`date-input-${key}-label`}
|
||||
className="calendar-new__day-label"
|
||||
className={styles.dayLabel}
|
||||
htmlFor={`date-input-${key}`}
|
||||
>
|
||||
{t("calendar:day", {
|
||||
|
|
@ -610,7 +610,7 @@ function TagsAdder() {
|
|||
<label htmlFor={id}>{t("calendar:forms.tags")}</label>
|
||||
<select
|
||||
id={id}
|
||||
className="calendar-new__select"
|
||||
className={styles.select}
|
||||
onChange={(e) =>
|
||||
setTags([...tags, e.target.value as CalendarEventTag])
|
||||
}
|
||||
|
|
@ -665,7 +665,7 @@ function BadgesAdder() {
|
|||
<label htmlFor={id}>{t("forms.badges")}</label>
|
||||
<select
|
||||
id={id}
|
||||
className="calendar-new__select"
|
||||
className={styles.select}
|
||||
onChange={(e) => {
|
||||
setBadges([
|
||||
...badges,
|
||||
|
|
@ -684,7 +684,7 @@ function BadgesAdder() {
|
|||
</select>
|
||||
</div>
|
||||
{badges.length > 0 && (
|
||||
<div className="calendar-new__badges">
|
||||
<div className={styles.badges}>
|
||||
{badges.map((badge) => (
|
||||
<div className="stack horizontal md items-center" key={badge.id}>
|
||||
<Badge badge={badge} isAnimated size={32} />
|
||||
|
|
@ -726,7 +726,7 @@ function AvatarImageInput({
|
|||
<img
|
||||
src={baseEvent.tournament.ctx.logoUrl}
|
||||
alt=""
|
||||
className="calendar-new__avatar-preview"
|
||||
className={styles.avatarPreview}
|
||||
/>
|
||||
<SendouButton
|
||||
variant="outlined"
|
||||
|
|
@ -782,7 +782,7 @@ function AvatarImageInput({
|
|||
<img
|
||||
src={URL.createObjectURL(avatarImg)}
|
||||
alt=""
|
||||
className="calendar-new__avatar-preview"
|
||||
className={styles.avatarPreview}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import type * as Changelog from "~/features/front-page/core/Changelog.server";
|
|||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import styles from "~/styles/front.module.css";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
BLANK_IMAGE_URL,
|
||||
|
|
@ -42,8 +41,8 @@ import {
|
|||
SENDOUQ_PAGE,
|
||||
sqHeaderGuyImageUrl,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import { type LeaderboardEntry, loader } from "../loaders/index.server";
|
||||
import styles from "./index.module.css";
|
||||
export { loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.support__table {
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
place-items: center;
|
||||
|
|
@ -6,12 +6,12 @@
|
|||
row-gap: var(--s-2);
|
||||
}
|
||||
|
||||
.support__checkmark {
|
||||
.checkmark {
|
||||
color: var(--color-success);
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.support__popover-trigger {
|
||||
.popoverTrigger {
|
||||
display: inline;
|
||||
height: 1rem;
|
||||
padding: 0;
|
||||
|
|
@ -12,8 +12,7 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import { SendouButton } from "../../../components/elements/Button";
|
||||
import { SendouPopover } from "../../../components/elements/Popover";
|
||||
|
||||
import "../support.css";
|
||||
import styles from "./support.module.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
|
|
@ -163,7 +162,7 @@ export default function SupportPage() {
|
|||
function SupportTable() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="support__table">
|
||||
<div className={styles.table}>
|
||||
<div />
|
||||
<div>Support</div>
|
||||
<div>Supporter</div>
|
||||
|
|
@ -178,7 +177,7 @@ function SupportTable() {
|
|||
{" "}
|
||||
<SendouPopover
|
||||
trigger={
|
||||
<SendouButton className="support__popover-trigger">
|
||||
<SendouButton className={styles.popoverTrigger}>
|
||||
?
|
||||
</SendouButton>
|
||||
}
|
||||
|
|
@ -190,7 +189,7 @@ function SupportTable() {
|
|||
</div>
|
||||
<div>
|
||||
{perk.tier === 1 ? (
|
||||
<CheckmarkIcon className="support__checkmark" />
|
||||
<CheckmarkIcon className={styles.checkmark} />
|
||||
) : null}
|
||||
</div>
|
||||
{perk.name === "badge" ? (
|
||||
|
|
@ -204,7 +203,7 @@ function SupportTable() {
|
|||
) : (
|
||||
<div>
|
||||
{perk.tier <= 2 ? (
|
||||
<CheckmarkIcon className="support__checkmark" />
|
||||
<CheckmarkIcon className={styles.checkmark} />
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -222,7 +221,7 @@ function SupportTable() {
|
|||
) : (
|
||||
<div>
|
||||
{perk.tier <= 3 ? (
|
||||
<CheckmarkIcon className="support__checkmark" />
|
||||
<CheckmarkIcon className={styles.checkmark} />
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { loader } from "../loaders/leaderboards.server";
|
|||
import type { XPLeaderboardItem } from "../queries/XPLeaderboard.server";
|
||||
export { loader };
|
||||
|
||||
import "../../top-search/top-search.css";
|
||||
import styles from "../../top-search/top-search.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["vods"],
|
||||
|
|
@ -241,7 +241,7 @@ function OwnEntryPeek({
|
|||
return (
|
||||
<div>
|
||||
{entry.firstOfTier ? (
|
||||
<div className="placements__tier-header">
|
||||
<div className={styles.tierHeader}>
|
||||
<TierImage tier={entry.firstOfTier} width={32} />
|
||||
{entry.firstOfTier.name}
|
||||
{entry.firstOfTier.isPlus ? "+" : ""}
|
||||
|
|
@ -250,24 +250,24 @@ function OwnEntryPeek({
|
|||
<div>
|
||||
<Link
|
||||
to={userSeasonsPage({ user: entry, season: data.season })}
|
||||
className="placements__table__row"
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<div className="placements__table__inner-row">
|
||||
<div className="placements__table__rank">{entry.placementRank}</div>
|
||||
<div className={styles.tableInnerRow}>
|
||||
<div className={styles.tableRank}>{entry.placementRank}</div>
|
||||
<div>
|
||||
<Avatar size="xxs" user={entry} />
|
||||
</div>
|
||||
{typeof entry.weaponSplId === "number" ? (
|
||||
<WeaponImage
|
||||
className="placements__table__weapon"
|
||||
className={styles.tableWeapon}
|
||||
variant="build"
|
||||
weaponSplId={entry.weaponSplId}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
) : null}
|
||||
<div className="placements__table__name">{entry.username}</div>
|
||||
<div className="placements__table__power">{entry.power}</div>
|
||||
<div className={styles.tableName}>{entry.username}</div>
|
||||
<div className={styles.tablePower}>{entry.power}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -294,7 +294,7 @@ function PlayersTable({
|
|||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="placements__table">
|
||||
<div className={styles.table}>
|
||||
{entries
|
||||
// hide normal rows that are showed in "fancy" top 10 format
|
||||
.filter((_, i) => !showingTopTen || i > 9)
|
||||
|
|
@ -302,7 +302,7 @@ function PlayersTable({
|
|||
return (
|
||||
<React.Fragment key={entry.entryId}>
|
||||
{entry.firstOfTier && showTiers ? (
|
||||
<div className="placements__tier-header">
|
||||
<div className={styles.tierHeader}>
|
||||
<TierImage tier={entry.firstOfTier} width={32} />
|
||||
{entry.firstOfTier.name}
|
||||
{entry.firstOfTier.isPlus ? "+" : ""}
|
||||
|
|
@ -310,33 +310,29 @@ function PlayersTable({
|
|||
) : null}
|
||||
<Link
|
||||
to={userSeasonsPage({ user: entry, season: data.season })}
|
||||
className="placements__table__row"
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<div className="placements__table__inner-row">
|
||||
<div className="placements__table__rank">
|
||||
{entry.placementRank}
|
||||
</div>
|
||||
<div className={styles.tableInnerRow}>
|
||||
<div className={styles.tableRank}>{entry.placementRank}</div>
|
||||
<div>
|
||||
<Avatar size="xxs" user={entry} />
|
||||
</div>
|
||||
{typeof entry.weaponSplId === "number" ? (
|
||||
<WeaponImage
|
||||
className="placements__table__weapon"
|
||||
className={styles.tableWeapon}
|
||||
variant="build"
|
||||
weaponSplId={entry.weaponSplId}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
) : null}
|
||||
<div className="placements__table__name">
|
||||
{entry.username}
|
||||
</div>
|
||||
<div className={styles.tableName}>{entry.username}</div>
|
||||
{entry.pendingPlusTier ? (
|
||||
<div className="text-xs text-theme whitespace-nowrap">
|
||||
➜ +{entry.pendingPlusTier}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="placements__table__power">
|
||||
<div className={styles.tablePower}>
|
||||
{entry.power.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -362,15 +358,13 @@ function TeamTable({
|
|||
_showQualificationDividers && isCurrentSeason && entries.length > 20;
|
||||
|
||||
return (
|
||||
<div className="placements__table">
|
||||
<div className={styles.table}>
|
||||
{entries.map((entry, i) => {
|
||||
return (
|
||||
<React.Fragment key={entry.entryId}>
|
||||
<div className="placements__table__row">
|
||||
<div className="placements__table__inner-row">
|
||||
<div className="placements__table__rank">
|
||||
{entry.placementRank}
|
||||
</div>
|
||||
<div className={styles.tableRow}>
|
||||
<div className={styles.tableInnerRow}>
|
||||
<div className={styles.tableRank}>{entry.placementRank}</div>
|
||||
{entry.team?.avatarUrl ? (
|
||||
<Link
|
||||
to={teamPage(entry.team.customUrl)}
|
||||
|
|
@ -379,7 +373,7 @@ function TeamTable({
|
|||
<Avatar
|
||||
size="xxs"
|
||||
url={entry.team.avatarUrl}
|
||||
className="placements__avatar"
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
|
|
@ -393,13 +387,15 @@ function TeamTable({
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="placements__table__power">
|
||||
<div className={styles.tablePower}>
|
||||
{entry.power.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{i === 11 && showQualificationDividers ? (
|
||||
<div className="placements__table__row placements__table__row__qualification">
|
||||
<div
|
||||
className={`${styles.tableRow} ${styles.tableRowQualification}`}
|
||||
>
|
||||
{t("common:leaderboard.qualification")}
|
||||
<InfoPopover tiny>
|
||||
{t("common:leaderboard.qualification.info")}
|
||||
|
|
@ -415,32 +411,28 @@ function TeamTable({
|
|||
|
||||
function XPTable({ entries }: { entries: XPLeaderboardItem[] }) {
|
||||
return (
|
||||
<div className="placements__table">
|
||||
<div className={styles.table}>
|
||||
{entries.map((entry) => {
|
||||
return (
|
||||
<Link
|
||||
to={topSearchPlayerPage(entry.playerId)}
|
||||
key={entry.entryId}
|
||||
className="placements__table__row"
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<div className="placements__table__inner-row">
|
||||
<div className="placements__table__rank">
|
||||
{entry.placementRank}
|
||||
</div>
|
||||
<div className={styles.tableInnerRow}>
|
||||
<div className={styles.tableRank}>{entry.placementRank}</div>
|
||||
{entry.discordId ? (
|
||||
<Avatar size="xxs" user={entry as any} />
|
||||
) : null}
|
||||
<WeaponImage
|
||||
className="placements__table__weapon"
|
||||
className={styles.tableWeapon}
|
||||
variant="build"
|
||||
weaponSplId={entry.weaponSplId}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div>{entry.name}</div>
|
||||
<div className="placements__table__power">
|
||||
{entry.power.toFixed(1)}
|
||||
</div>
|
||||
<div className={styles.tablePower}>{entry.power.toFixed(1)}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
127
app/features/map-planner/components/Planner.module.css
Normal file
127
app/features/map-planner/components/Planner.module.css
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
.topSection {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-3);
|
||||
border: 2px solid var(--color-border);
|
||||
border-top: transparent;
|
||||
border-radius: 0 0 var(--rounded) var(--rounded);
|
||||
background-color: var(--color-bg);
|
||||
gap: var(--s-4);
|
||||
transform: translate(-50%, -42%);
|
||||
}
|
||||
|
||||
.outlineToggle {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 10%;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.outlineToggleButton {
|
||||
background-color: var(--color-bg-high);
|
||||
color: var(--color-text);
|
||||
font-size: var(--fonts-xs);
|
||||
padding: var(--s-2);
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.outlineToggleButtonOutlined {
|
||||
background-color: var(--color-text-accent);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.weaponsSection {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 15%;
|
||||
width: 150px;
|
||||
max-height: 85vh;
|
||||
border: 2px solid var(--color-border);
|
||||
border-left: transparent;
|
||||
border-radius: 0 var(--rounded) var(--rounded) 0;
|
||||
background: var(--color-bg);
|
||||
gap: 2px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.weaponsSectionWide {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.weaponsSummary {
|
||||
background-color: var(--color-bg-high);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--bold);
|
||||
padding: var(--s-2-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.weaponsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: var(--s-1-5);
|
||||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.stylePanel {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.zoomQuickActions {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zoomMenu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.quickActions {
|
||||
display: flex;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.draggableButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--s-0-5);
|
||||
border: none;
|
||||
border-radius: var(--rounded);
|
||||
background: transparent;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.draggableButton:hover {
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.weaponDragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dragPreviewContainer {
|
||||
display: block;
|
||||
min-width: 45px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.dragPreview {
|
||||
cursor: grabbing;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ import {
|
|||
import { SendouButton } from "../../../components/elements/Button";
|
||||
import { Image } from "../../../components/Image";
|
||||
import type { StageBackgroundStyle } from "../plans-types";
|
||||
import styles from "./Planner.module.css";
|
||||
|
||||
const DROPPED_IMAGE_SIZE_PX = 45;
|
||||
const BACKGROUND_WIDTH = 1127;
|
||||
|
|
@ -252,8 +253,8 @@ export default function Planner() {
|
|||
width={DROPPED_IMAGE_SIZE_PX}
|
||||
height={DROPPED_IMAGE_SIZE_PX}
|
||||
alt=""
|
||||
className="plans__drag-preview"
|
||||
containerClassName="plans__drag-preview-container"
|
||||
className={styles.dragPreview}
|
||||
containerClassName={styles.dragPreviewContainer}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
|
|
@ -264,13 +265,13 @@ export default function Planner() {
|
|||
// Formats the style panel so it can have classnames, this is needed so it can be moved below the header bar which blocks clicks (idk why this is different to the old version), also needed to format the quick actions bar and zoom menu nicely
|
||||
function CustomStylePanel(props: TLUiStylePanelProps) {
|
||||
return (
|
||||
<div className="plans__style-panel">
|
||||
<div className={styles.stylePanel}>
|
||||
<DefaultStylePanel {...props} />
|
||||
<div className="plans__zoom-quick-actions">
|
||||
<div className="plans__quick-actions">
|
||||
<div className={styles.zoomQuickActions}>
|
||||
<div className={styles.quickActions}>
|
||||
<DefaultQuickActions />
|
||||
</div>
|
||||
<div className="plans__zoom-menu">
|
||||
<div className={styles.zoomMenu}>
|
||||
<DefaultZoomMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -292,13 +293,14 @@ function OutlineToggle({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="plans__outline-toggle">
|
||||
<div className={styles.outlineToggle}>
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
onPress={handleClick}
|
||||
className={clsx("plans__outline-toggle__button", {
|
||||
"plans__outline-toggle__button__outlined": outlined,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.outlineToggleButton,
|
||||
outlined && styles.outlineToggleButtonOutlined,
|
||||
)}
|
||||
>
|
||||
{outlined
|
||||
? t("common:actions.outlined")
|
||||
|
|
@ -334,9 +336,10 @@ function DraggableWeaponButton({
|
|||
<button
|
||||
type="button"
|
||||
ref={setNodeRef}
|
||||
className={clsx("plans__draggable-button", {
|
||||
"plans__weapon-dragging": isDragging,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.draggableButton,
|
||||
isDragging && styles.weaponDragging,
|
||||
)}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
|
|
@ -358,14 +361,16 @@ function WeaponImageSelector() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx("plans__weapons-section scrollbar", {
|
||||
"plans__weapons-section__wide": isWide,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.weaponsSection,
|
||||
"scrollbar",
|
||||
isWide && styles.weaponsSectionWide,
|
||||
)}
|
||||
>
|
||||
{weaponCategories.map((category) => {
|
||||
return (
|
||||
<details key={category.name}>
|
||||
<summary className="plans__weapons-summary">
|
||||
<summary className={styles.weaponsSummary}>
|
||||
<Image
|
||||
path={weaponCategoryUrl(category.name)}
|
||||
width={24}
|
||||
|
|
@ -374,7 +379,7 @@ function WeaponImageSelector() {
|
|||
/>
|
||||
{t(`common:weapon.category.${category.name}`)}
|
||||
</summary>
|
||||
<div className="plans__weapons-container">
|
||||
<div className={styles.weaponsContainer}>
|
||||
{category.weaponIds.map((weaponId) => {
|
||||
return (
|
||||
<DraggableWeaponButton
|
||||
|
|
@ -394,11 +399,11 @@ function WeaponImageSelector() {
|
|||
);
|
||||
})}
|
||||
<details>
|
||||
<summary className="plans__weapons-summary">
|
||||
<summary className={styles.weaponsSummary}>
|
||||
<Image path={subWeaponImageUrl(0)} width={24} height={24} alt="" />
|
||||
{t("common:weapon.category.subs")}
|
||||
</summary>
|
||||
<div className="plans__weapons-container">
|
||||
<div className={styles.weaponsContainer}>
|
||||
{subWeaponIds.map((subWeaponId) => {
|
||||
return (
|
||||
<DraggableWeaponButton
|
||||
|
|
@ -416,7 +421,7 @@ function WeaponImageSelector() {
|
|||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary className="plans__weapons-summary">
|
||||
<summary className={styles.weaponsSummary}>
|
||||
<Image
|
||||
path={specialWeaponImageUrl(1)}
|
||||
width={24}
|
||||
|
|
@ -425,7 +430,7 @@ function WeaponImageSelector() {
|
|||
/>
|
||||
{t("common:weapon.category.specials")}
|
||||
</summary>
|
||||
<div className="plans__weapons-container">
|
||||
<div className={styles.weaponsContainer}>
|
||||
{specialWeaponIds.map((specialWeaponId) => {
|
||||
return (
|
||||
<DraggableWeaponButton
|
||||
|
|
@ -443,11 +448,11 @@ function WeaponImageSelector() {
|
|||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary className="plans__weapons-summary">
|
||||
<summary className={styles.weaponsSummary}>
|
||||
<Image path={modeImageUrl("RM")} width={24} height={24} alt="" />
|
||||
{t("common:plans.adder.objective")}
|
||||
</summary>
|
||||
<div className="plans__weapons-container">
|
||||
<div className={styles.weaponsContainer}>
|
||||
{(["TC", "RM", "CB"] as const).map((mode) => {
|
||||
return (
|
||||
<DraggableWeaponButton
|
||||
|
|
@ -489,7 +494,7 @@ function StageBackgroundSelector({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="plans__top-section">
|
||||
<div className={styles.topSection}>
|
||||
<select
|
||||
className="w-max"
|
||||
value={stageId}
|
||||
|
|
|
|||
|
|
@ -75,105 +75,6 @@ div {
|
|||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.plans__top-section {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-3);
|
||||
border: 2px solid var(--color-border);
|
||||
border-top: transparent;
|
||||
border-radius: 0 0 var(--rounded) var(--rounded);
|
||||
background-color: var(--color-bg);
|
||||
gap: var(--s-4);
|
||||
transform: translate(-50%, -42%);
|
||||
}
|
||||
|
||||
.plans__outline-toggle {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 10%;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.plans__outline-toggle > .plans__outline-toggle__button {
|
||||
background-color: var(--color-bg-high);
|
||||
color: var(--color-text);
|
||||
font-size: var(--fonts-xs);
|
||||
padding: var(--s-2);
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.plans__outline-toggle > .plans__outline-toggle__button__outlined {
|
||||
background-color: var(--color-text-accent);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.plans__weapons-section {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 15%;
|
||||
width: 150px;
|
||||
max-height: 85vh;
|
||||
border: 2px solid var(--color-border);
|
||||
border-left: transparent;
|
||||
border-radius: 0 var(--rounded) var(--rounded) 0;
|
||||
background: var(--color-bg);
|
||||
gap: 2px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plans__weapons-section__wide {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.plans__weapons-summary {
|
||||
background-color: var(--color-bg-high);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--bold);
|
||||
padding: var(--s-2-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.plans__weapons-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: var(--s-1-5);
|
||||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.plans__no-img-text {
|
||||
color: var(--color-error);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.plans__style-panel {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.plans__zoom-quick-actions {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plans__zoom-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.plans__quick-actions {
|
||||
display: flex;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
img[src$="?outline=red"] {
|
||||
--outline-width: 0.1rem;
|
||||
--outline-color: crimson;
|
||||
|
|
@ -181,37 +82,3 @@ img[src$="?outline=red"] {
|
|||
drop-shadow(0 0 var(--outline-width) var(--outline-color))
|
||||
drop-shadow(0 0 var(--outline-width) var(--outline-color));
|
||||
}
|
||||
|
||||
.plans__draggable-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--s-0-5);
|
||||
border: none;
|
||||
border-radius: var(--rounded);
|
||||
background: transparent;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.plans__draggable-button:hover {
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.plans__weapon-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.plans__drag-preview-container {
|
||||
display: block;
|
||||
min-width: 45px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.plans__drag-preview {
|
||||
cursor: grabbing;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { metaTags } from "~/utils/remix";
|
|||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { navIconUrl, PLANNER_URL } from "~/utils/urls";
|
||||
|
||||
import "../plans.css";
|
||||
import "../plans-global.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
.object-damage__controls {
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.object-damage__selects {
|
||||
.selects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-5);
|
||||
}
|
||||
|
||||
.object-damage__selects__weapon {
|
||||
.selectsWeapon {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.object-damage__ability {
|
||||
.ability {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.object-damage__ap-label {
|
||||
.apLabel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.object-damage__grid {
|
||||
.grid {
|
||||
column-gap: var(--s-2);
|
||||
display: grid;
|
||||
max-height: 60vh;
|
||||
|
|
@ -34,17 +34,17 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 431px) {
|
||||
.object-damage__grid {
|
||||
.grid {
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
.object-damage__hp {
|
||||
.hp {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.object-damage__table-header {
|
||||
.tableHeader {
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-higher);
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -65,23 +65,23 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.object-damage__weapon-image {
|
||||
.weaponImage {
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.object-damage__distance {
|
||||
.distance {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
margin-block-start: var(--s-0-5);
|
||||
}
|
||||
|
||||
.object-damage__receiver-image {
|
||||
.receiverImage {
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.object-damage__table-card {
|
||||
.tableCard {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -93,20 +93,20 @@
|
|||
place-items: center;
|
||||
}
|
||||
|
||||
.object-damage__table-card__results {
|
||||
.tableCardResults {
|
||||
display: grid;
|
||||
column-gap: var(--s-2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.object-damage__abbr {
|
||||
.abbr {
|
||||
color: var(--color-text-high);
|
||||
font-weight: var(--bold);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.object-damage__multiplier {
|
||||
.multiplier {
|
||||
color: var(--color-text-accent);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
|
|
@ -114,14 +114,14 @@
|
|||
margin-block-start: var(--s-2);
|
||||
}
|
||||
|
||||
.object-damage__bottom-container {
|
||||
.bottomContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.object-damage__patch {
|
||||
.patch {
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-accent-low);
|
||||
color: var(--color-accent-high);
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { Image, WeaponImage } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import type { DamageType } from "~/features/build-analyzer/analyzer-types";
|
||||
import { possibleApValues } from "~/features/build-analyzer/core/utils";
|
||||
import {
|
||||
BIG_BUBBLER_ID,
|
||||
BOOYAH_BOMB_ID,
|
||||
|
|
@ -19,6 +24,8 @@ import {
|
|||
TRIPLE_SPLASHDOWN_ID,
|
||||
WAVE_BREAKER_ID,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
mainWeaponImageUrl,
|
||||
|
|
@ -32,14 +39,7 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import { useObjectDamage } from "../calculator-hooks";
|
||||
import type { DamageReceiver } from "../calculator-types";
|
||||
import "../calculator.css";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import type { DamageType } from "~/features/build-analyzer/analyzer-types";
|
||||
import { possibleApValues } from "~/features/build-analyzer/core/utils";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import styles from "./object-damage-calculator.module.css";
|
||||
|
||||
export const CURRENT_PATCH = "10.1";
|
||||
|
||||
|
|
@ -79,9 +79,9 @@ export default function ObjectDamagePage() {
|
|||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div className="object-damage__controls">
|
||||
<div className="object-damage__selects">
|
||||
<div className="object-damage__selects__weapon">
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.selects}>
|
||||
<div className={styles.selectsWeapon}>
|
||||
<Label htmlFor="weapon">{t("analyzer:labels.weapon")}</Label>
|
||||
<WeaponSelect
|
||||
includeSubSpecial
|
||||
|
|
@ -148,11 +148,11 @@ export default function ObjectDamagePage() {
|
|||
) : (
|
||||
<div>{t("analyzer:noDmgData")}</div>
|
||||
)}
|
||||
<div className="object-damage__bottom-container">
|
||||
<div className={styles.bottomContainer}>
|
||||
<div className="text-lighter text-xs">
|
||||
{t("analyzer:dmgHtdExplanation")}
|
||||
</div>
|
||||
<div className="object-damage__patch">
|
||||
<div className={styles.patch}>
|
||||
{t("analyzer:patch")} {CURRENT_PATCH}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -226,14 +226,12 @@ const damageReceiverImages: Record<DamageReceiver, string> = {
|
|||
|
||||
const damageReceiverAp: Partial<Record<DamageReceiver, JSX.Element>> = {
|
||||
GreatBarrier_Barrier: (
|
||||
<Ability ability="SPU" size="TINY" className="object-damage__ability" />
|
||||
<Ability ability="SPU" size="TINY" className={styles.ability} />
|
||||
),
|
||||
GreatBarrier_WeakPoint: (
|
||||
<Ability ability="SPU" size="TINY" className="object-damage__ability" />
|
||||
),
|
||||
Wsb_Shield: (
|
||||
<Ability ability="BRU" size="TINY" className="object-damage__ability" />
|
||||
<Ability ability="SPU" size="TINY" className={styles.ability} />
|
||||
),
|
||||
Wsb_Shield: <Ability ability="BRU" size="TINY" className={styles.ability} />,
|
||||
};
|
||||
|
||||
function DamageReceiversGrid({
|
||||
|
|
@ -253,7 +251,7 @@ function DamageReceiversGrid({
|
|||
return (
|
||||
<div>
|
||||
<div
|
||||
className="object-damage__grid scrollbar"
|
||||
className={`${styles.grid} scrollbar`}
|
||||
style={{
|
||||
gridTemplateColumns: gridTemplateColumnsValue(
|
||||
damagesToReceivers[0]?.damages.length ?? 0,
|
||||
|
|
@ -261,13 +259,13 @@ function DamageReceiversGrid({
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className="object-damage__table-header"
|
||||
className={styles.tableHeader}
|
||||
style={{ zIndex: "1", justifyContent: "center" }}
|
||||
>
|
||||
<div>
|
||||
<Label htmlFor="ap">
|
||||
{t("analyzer:labels.amountOf")}
|
||||
<div className="object-damage__ap-label">
|
||||
<div className={styles.apLabel}>
|
||||
<Ability ability="BRU" size="TINY" />
|
||||
<Ability ability="SPU" size="TINY" />
|
||||
</div>
|
||||
|
|
@ -276,7 +274,7 @@ function DamageReceiversGrid({
|
|||
<div>{children}</div>
|
||||
</div>
|
||||
{damagesToReceivers[0]?.damages.map((damage) => (
|
||||
<div key={damage.id} className="object-damage__table-header">
|
||||
<div key={damage.id} className={styles.tableHeader}>
|
||||
{t(`weapons:${weapon.type}_${weapon.id}` as any)}
|
||||
<div className="text-lighter stack horizontal sm justify-center items-center">
|
||||
{weapon.type === "MAIN" ? (
|
||||
|
|
@ -285,7 +283,7 @@ function DamageReceiversGrid({
|
|||
width={24}
|
||||
height={24}
|
||||
variant="build"
|
||||
className="object-damage__weapon-image"
|
||||
className={styles.weaponImage}
|
||||
/>
|
||||
) : weapon.type === "SUB" ? (
|
||||
<Image
|
||||
|
|
@ -293,7 +291,7 @@ function DamageReceiversGrid({
|
|||
path={subWeaponImageUrl(weapon.id)}
|
||||
width={24}
|
||||
height={24}
|
||||
className="object-damage__weapon-image"
|
||||
className={styles.weaponImage}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
|
|
@ -301,14 +299,12 @@ function DamageReceiversGrid({
|
|||
path={specialWeaponImageUrl(weapon.id)}
|
||||
width={24}
|
||||
height={24}
|
||||
className="object-damage__weapon-image"
|
||||
className={styles.weaponImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("object-damage__distance", {
|
||||
invisible: !damage.distance,
|
||||
})}
|
||||
className={clsx(styles.distance, !damage.distance && "invisible")}
|
||||
>
|
||||
{t("analyzer:distanceInline", {
|
||||
value: Array.isArray(damage.distance)
|
||||
|
|
@ -325,16 +321,16 @@ function DamageReceiversGrid({
|
|||
{damagesToReceivers.map((damageToReceiver, i) => {
|
||||
return (
|
||||
<React.Fragment key={damageToReceiver.receiver}>
|
||||
<div className="object-damage__table-header">
|
||||
<div className={styles.tableHeader}>
|
||||
<div>
|
||||
<Label htmlFor="ap">
|
||||
<div className="object-damage__ap-label">
|
||||
<div className={styles.apLabel}>
|
||||
{abilityPoints !== "0" &&
|
||||
damageReceiverAp[damageToReceiver.receiver]}
|
||||
</div>
|
||||
</Label>
|
||||
<Image
|
||||
className="object-damage__receiver-image"
|
||||
className={styles.receiverImage}
|
||||
key={i}
|
||||
alt=""
|
||||
path={damageReceiverImages[damageToReceiver.receiver]}
|
||||
|
|
@ -342,7 +338,7 @@ function DamageReceiversGrid({
|
|||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="object-damage__hp">
|
||||
<div className={styles.hp}>
|
||||
<span data-testid={`hp-${damageToReceiver.receiver}`}>
|
||||
{roundToNDecimalPlaces(damageToReceiver.hitPoints)}
|
||||
</span>
|
||||
|
|
@ -351,10 +347,10 @@ function DamageReceiversGrid({
|
|||
</div>
|
||||
{damageToReceiver.damages.map((damage) => {
|
||||
return (
|
||||
<div key={damage.id} className="object-damage__table-card">
|
||||
<div className="object-damage__table-card__results">
|
||||
<div key={damage.id} className={styles.tableCard}>
|
||||
<div className={styles.tableCardResults}>
|
||||
<abbr
|
||||
className="object-damage__abbr"
|
||||
className={styles.abbr}
|
||||
title={t("analyzer:stat.category.damage")}
|
||||
>
|
||||
{t("analyzer:damageShort")}
|
||||
|
|
@ -367,7 +363,7 @@ function DamageReceiversGrid({
|
|||
{damage.value}
|
||||
</div>
|
||||
<abbr
|
||||
className="object-damage__abbr"
|
||||
className={styles.abbr}
|
||||
title={t("analyzer:hitsToDestroyLong")}
|
||||
>
|
||||
{t("analyzer:hitsToDestroyShort")}
|
||||
|
|
@ -380,7 +376,7 @@ function DamageReceiversGrid({
|
|||
{damage.hitsToDestroy}
|
||||
</div>
|
||||
</div>
|
||||
<div className="object-damage__multiplier">
|
||||
<div className={styles.multiplier}>
|
||||
×{damage.multiplier}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,102 +1,102 @@
|
|||
.plus__container {
|
||||
.container {
|
||||
max-width: 24rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.plus__suggested-info-text {
|
||||
.suggestedInfoText {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.plus__top-container {
|
||||
.topContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.plus__top-container.content-centered {
|
||||
.topContainerCentered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plus__radios {
|
||||
.radios {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--s-5);
|
||||
}
|
||||
|
||||
.plus__radio-container {
|
||||
.radioContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.plus__radio-label {
|
||||
.radioLabel {
|
||||
font-size: var(--fonts-sm);
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.plus__users-count {
|
||||
.usersCount {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.plus__suggested-user-info {
|
||||
.suggestedUserInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.plus__comment {
|
||||
.comment {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.plus__comment-button {
|
||||
.commentButton {
|
||||
max-width: 12rem;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.plus__view-comments-action {
|
||||
.viewCommentsAction {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.plus__comment-time {
|
||||
.commentTime {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.plus__delete-button {
|
||||
.deleteButton {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.plus__modal-select {
|
||||
.modalSelect {
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.plus__modal-textarea {
|
||||
.modalTextarea {
|
||||
width: 100% !important;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.plus-voting__container {
|
||||
.votingContainer {
|
||||
max-width: 32rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.plus-voting__vote-button {
|
||||
.votingVoteButton {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.plus-voting__vote-button.downvote {
|
||||
.votingVoteButtonDownvote {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
outline-color: var(--color-error);
|
||||
}
|
||||
|
||||
.plus-voting__submit-button {
|
||||
.votingSubmitButton {
|
||||
display: flex;
|
||||
width: 12rem;
|
||||
height: 2.5rem;
|
||||
|
|
@ -104,11 +104,11 @@
|
|||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.plus-voting__progress {
|
||||
.votingProgress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plus-voting__alert {
|
||||
.votingAlert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -124,12 +124,12 @@
|
|||
padding-inline: var(--s-3) var(--s-4);
|
||||
}
|
||||
|
||||
.plus-voting__alert > svg {
|
||||
.votingAlert > svg {
|
||||
height: 1.75rem;
|
||||
fill: var(--color-success);
|
||||
}
|
||||
|
||||
.plus-voting__bio-header {
|
||||
.votingBioHeader {
|
||||
font-size: var(--fonts-lg);
|
||||
padding-block-end: var(--s-2);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { SubmitButton } from "~/components/SubmitButton";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { plusSuggestionPage } from "~/utils/urls";
|
||||
import { action } from "../actions/plus.suggestions.new.server";
|
||||
import styles from "../plus.module.css";
|
||||
import { PLUS_SUGGESTION, PLUS_TIERS } from "../plus-suggestions-constants";
|
||||
import { canSuggestNewUser } from "../plus-suggestions-utils";
|
||||
import type { PlusSuggestionsLoaderData } from "./plus.suggestions";
|
||||
|
|
@ -50,7 +51,7 @@ export default function PlusNewSuggestionModalPage() {
|
|||
<select
|
||||
id="tier"
|
||||
name="tier"
|
||||
className="plus__modal-select"
|
||||
className={styles.modalSelect}
|
||||
value={targetPlusTier}
|
||||
onChange={(e) => setTargetPlusTier(Number(e.target.value))}
|
||||
>
|
||||
|
|
@ -107,7 +108,7 @@ export function CommentTextarea({ maxLength }: { maxLength: number }) {
|
|||
<textarea
|
||||
id="comment"
|
||||
name="comment"
|
||||
className="plus__modal-textarea"
|
||||
className={styles.modalTextarea}
|
||||
rows={4}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { metaTags } from "~/utils/remix";
|
|||
import { userPage } from "~/utils/urls";
|
||||
import { action } from "../actions/plus.suggestions.server";
|
||||
import { loader } from "../loaders/plus.suggestions.server";
|
||||
import styles from "../plus.module.css";
|
||||
import {
|
||||
canAddCommentToSuggestionFE,
|
||||
canDeleteComment,
|
||||
|
|
@ -71,7 +72,7 @@ export default function PlusSuggestionsPage() {
|
|||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<div className="plus__container">
|
||||
<div className={styles.container}>
|
||||
<div className="stack md">
|
||||
<SuggestedForInfo />
|
||||
{searchParams.get("alert") === "true" ? (
|
||||
|
|
@ -82,14 +83,14 @@ export default function PlusSuggestionsPage() {
|
|||
) : null}
|
||||
<div className="stack lg">
|
||||
<div
|
||||
className={clsx("plus__top-container", {
|
||||
"content-centered": !canSuggestNewUser({
|
||||
className={clsx(styles.topContainer, {
|
||||
[styles.topContainerCentered]: !canSuggestNewUser({
|
||||
user,
|
||||
suggestions: data.suggestions,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<div className="plus__radios">
|
||||
<div className={styles.radios}>
|
||||
{[1, 2, 3].map((tier) => {
|
||||
const id = String(tier);
|
||||
const suggestions = data.suggestions.filter(
|
||||
|
|
@ -97,10 +98,10 @@ export default function PlusSuggestionsPage() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div key={id} className="plus__radio-container">
|
||||
<label htmlFor={id} className="plus__radio-label">
|
||||
<div key={id} className={styles.radioContainer}>
|
||||
<label htmlFor={id} className={styles.radioLabel}>
|
||||
+{tier}{" "}
|
||||
<span className="plus__users-count">
|
||||
<span className={styles.usersCount}>
|
||||
({suggestions.length})
|
||||
</span>
|
||||
</label>
|
||||
|
|
@ -129,7 +130,7 @@ export default function PlusSuggestionsPage() {
|
|||
);
|
||||
})}
|
||||
{visibleSuggestions.length === 0 ? (
|
||||
<div className="plus__suggested-info-text text-center">
|
||||
<div className={clsx(styles.suggestedInfoText, "text-center")}>
|
||||
No suggestions yet
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -204,7 +205,7 @@ function SuggestedUser({
|
|||
|
||||
return (
|
||||
<div className="stack md">
|
||||
<div className="plus__suggested-user-info">
|
||||
<div className={styles.suggestedUserInfo}>
|
||||
<Avatar user={suggestion.suggested} size="md" />
|
||||
<h2>
|
||||
<Link className="all-unset" to={userPage(suggestion.suggested)}>
|
||||
|
|
@ -218,7 +219,7 @@ function SuggestedUser({
|
|||
targetPlusTier: Number(tier),
|
||||
}) ? (
|
||||
<LinkButton
|
||||
className="plus__comment-button"
|
||||
className={styles.commentButton}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
to={`comment/${tier}/${suggestion.suggested.id}?tier=${tier}`}
|
||||
|
|
@ -257,17 +258,17 @@ export function PlusSuggestionComments({
|
|||
}) {
|
||||
return (
|
||||
<details open={defaultOpen} className="w-full">
|
||||
<summary className="plus__view-comments-action">
|
||||
<summary className={styles.viewCommentsAction}>
|
||||
Comments ({suggestion.entries.length})
|
||||
</summary>
|
||||
<div className="stack sm mt-2">
|
||||
{suggestion.entries.map((entry) => {
|
||||
return (
|
||||
<fieldset key={entry.id} className="plus__comment">
|
||||
<fieldset key={entry.id} className={styles.comment}>
|
||||
<legend>{entry.author.username}</legend>
|
||||
{entry.text}
|
||||
<div className="stack horizontal xs items-center">
|
||||
<span className="plus__comment-time">
|
||||
<span className={styles.commentTime}>
|
||||
<RelativeTime
|
||||
timestamp={databaseTimestampToDate(
|
||||
entry.createdAt,
|
||||
|
|
@ -325,7 +326,7 @@ function CommentDeleteButton({
|
|||
}
|
||||
>
|
||||
<SendouButton
|
||||
className="plus__delete-button"
|
||||
className={styles.deleteButton}
|
||||
icon={<TrashIcon />}
|
||||
variant="minimal-destructive"
|
||||
aria-label="Delete comment"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
plusSuggestionsNewPage,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import "~/styles/plus.css";
|
||||
import "../plus.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
navItemName: "plus",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.plus-history__own-scores {
|
||||
.ownScores {
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
margin: 0 auto;
|
||||
|
|
@ -10,48 +10,48 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.plus-history__success {
|
||||
.success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.plus-history__fail {
|
||||
.fail {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.plus-history__tier-header {
|
||||
.tierHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.plus-history__tier-header::before,
|
||||
.plus-history__tier-header::after {
|
||||
.tierHeader::before,
|
||||
.tierHeader::after {
|
||||
flex: 1 1;
|
||||
border-bottom: 3px solid var(--color-accent-low);
|
||||
margin: auto;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.plus-history__tier-header::before {
|
||||
.tierHeader::before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.plus-history__tier-header::after {
|
||||
.tierHeader::after {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.plus-history__passed-info-container {
|
||||
.passedInfoContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.plus-history__passed-header {
|
||||
.passedHeader {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.plus-history__user-status {
|
||||
.userStatus {
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-inverse);
|
||||
|
|
@ -60,12 +60,12 @@
|
|||
padding-inline: var(--s-1-5);
|
||||
}
|
||||
|
||||
.plus-history__user-status.failed {
|
||||
.userStatusFailed {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.plus-history__suggestion-s {
|
||||
.suggestionS {
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
|
|
@ -5,10 +5,9 @@ import { metaTags } from "~/utils/remix";
|
|||
import { PLUS_SERVER_DISCORD_URL, userPage } from "~/utils/urls";
|
||||
|
||||
import { loader } from "../loaders/plus.voting.results.server";
|
||||
import styles from "../plus-voting-results.module.css";
|
||||
export { loader };
|
||||
|
||||
import "~/styles/plus-history.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Plus Server voting results",
|
||||
|
|
@ -30,14 +29,14 @@ export default function PlusVotingResultsPage() {
|
|||
</h2>
|
||||
{data.ownScores && data.ownScores.length > 0 ? (
|
||||
<>
|
||||
<ul className="plus-history__own-scores stack sm">
|
||||
<ul className={clsx(styles.ownScores, "stack sm")}>
|
||||
{data.ownScores.map((result) => (
|
||||
<li key={result.tier}>
|
||||
You{" "}
|
||||
{result.passedVoting ? (
|
||||
<span className="plus-history__success">passed</span>
|
||||
<span className={styles.success}>passed</span>
|
||||
) : (
|
||||
<span className="plus-history__fail">didn't pass</span>
|
||||
<span className={styles.fail}>didn't pass</span>
|
||||
)}{" "}
|
||||
the +{result.tier} voting
|
||||
{typeof result.score === "number"
|
||||
|
|
@ -81,25 +80,25 @@ function Results({
|
|||
<div className="stack lg">
|
||||
{results.map((tiersResults) => (
|
||||
<div className="stack md" key={tiersResults.tier}>
|
||||
<h3 className="plus-history__tier-header">
|
||||
<h3 className={styles.tierHeader}>
|
||||
<span>+{tiersResults.tier}</span>
|
||||
</h3>
|
||||
{(["passed", "failed"] as const).map((status) => (
|
||||
<div key={status} className="plus-history__passed-info-container">
|
||||
<h4 className="plus-history__passed-header">
|
||||
<div key={status} className={styles.passedInfoContainer}>
|
||||
<h4 className={styles.passedHeader}>
|
||||
{status === "passed" ? "Passed" : "Didn't pass"} (
|
||||
{tiersResults[status].length})
|
||||
</h4>
|
||||
{tiersResults[status].map((user) => (
|
||||
<Link
|
||||
to={userPage(user)}
|
||||
className={clsx("plus-history__user-status", {
|
||||
failed: status === "failed",
|
||||
className={clsx(styles.userStatus, {
|
||||
[styles.userStatusFailed]: status === "failed",
|
||||
})}
|
||||
key={user.id}
|
||||
>
|
||||
{user.wasSuggested ? (
|
||||
<span className="plus-history__suggestion-s">S</span>
|
||||
<span className={styles.suggestionS}>S</span>
|
||||
) : null}
|
||||
{user.username}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { CheckmarkIcon } from "~/components/icons/Checkmark";
|
||||
import { RelativeTime } from "~/components/RelativeTime";
|
||||
import styles from "~/features/plus-suggestions/plus.module.css";
|
||||
import { usePlusVoting } from "~/features/plus-voting/core";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
|
|
@ -53,7 +55,7 @@ function VotingTimingInfo(
|
|||
return (
|
||||
<div className="stack md">
|
||||
{data.voted ? (
|
||||
<div className="plus-voting__alert">
|
||||
<div className={styles.votingAlert}>
|
||||
<CheckmarkIcon /> You have voted
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -83,7 +85,7 @@ function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
|
|||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<div className="plus-voting__container stack md">
|
||||
<div className={clsx(styles.votingContainer, "stack md")}>
|
||||
<div className="stack xs">
|
||||
<div className="text-sm text-center">
|
||||
Voting ends{" "}
|
||||
|
|
@ -93,7 +95,7 @@ function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
|
|||
</div>
|
||||
{progress ? (
|
||||
<progress
|
||||
className="plus-voting__progress"
|
||||
className={styles.votingProgress}
|
||||
value={progress[0]}
|
||||
max={progress[1]}
|
||||
title={`Voting progress ${progress[0]} out of ${progress[1]}`}
|
||||
|
|
@ -125,14 +127,17 @@ function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
|
|||
<h2>{currentUser.user.username}</h2>
|
||||
<div className="stack horizontal lg">
|
||||
<SendouButton
|
||||
className="plus-voting__vote-button downvote"
|
||||
className={clsx(
|
||||
styles.votingVoteButton,
|
||||
styles.votingVoteButtonDownvote,
|
||||
)}
|
||||
variant="outlined"
|
||||
onPress={() => addVote("downvote")}
|
||||
>
|
||||
-1
|
||||
</SendouButton>
|
||||
<SendouButton
|
||||
className="plus-voting__vote-button"
|
||||
className={styles.votingVoteButton}
|
||||
variant="outlined"
|
||||
onPress={() => addVote("upvote")}
|
||||
>
|
||||
|
|
@ -147,7 +152,7 @@ function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
|
|||
) : null}
|
||||
{currentUser.user.bio ? (
|
||||
<article className="w-full">
|
||||
<h2 className="plus-voting__bio-header">Bio</h2>
|
||||
<h2 className={styles.votingBioHeader}>Bio</h2>
|
||||
{currentUser.user.bio}
|
||||
</article>
|
||||
) : null}
|
||||
|
|
@ -155,7 +160,7 @@ function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
|
|||
) : (
|
||||
<Form method="post">
|
||||
<input type="hidden" name="votes" value={JSON.stringify(votes)} />
|
||||
<SendouButton className="plus-voting__submit-button" type="submit">
|
||||
<SendouButton className={styles.votingSubmitButton} type="submit">
|
||||
Submit votes
|
||||
</SendouButton>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.q-settings__radio {
|
||||
.radio {
|
||||
background-color: var(--color-bg-high);
|
||||
font-size: var(--fonts-xs);
|
||||
text-transform: uppercase;
|
||||
|
|
@ -12,25 +12,25 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.q-settings__radio__emoji {
|
||||
.radioEmoji {
|
||||
filter: grayscale(100%);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.q-settings__radio__checked {
|
||||
.radioChecked {
|
||||
color: var(--color-text);
|
||||
outline: 2px solid var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.q-settings__radio__checked .q-settings__radio__emoji {
|
||||
.radioChecked .radioEmoji {
|
||||
filter: grayscale(0%);
|
||||
}
|
||||
|
||||
.q-settings__radio:hover .q-settings__radio__emoji {
|
||||
.radio:hover .radioEmoji {
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
.q-settings__summary {
|
||||
.summary {
|
||||
padding: var(--s-3);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-high);
|
||||
|
|
@ -40,11 +40,11 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.q-settings__summary > div {
|
||||
.summary > div {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.q-settings__summary svg {
|
||||
.summary svg {
|
||||
width: 24px;
|
||||
color: var(--color-text-accent);
|
||||
position: absolute;
|
||||
|
|
@ -52,21 +52,21 @@
|
|||
top: 14px;
|
||||
}
|
||||
|
||||
.q-settings__weapon-pool-select-container {
|
||||
.weaponPoolSelectContainer {
|
||||
width: 250px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.q-settings__volume-slider-icon {
|
||||
.volumeSliderIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Necessary because default style adds padding, making the slider not go from 0 to 1 visually
|
||||
Changing the default style would affect all other input elements, so we need to override it
|
||||
*/
|
||||
.q-settings__volume-slider-input {
|
||||
.volumeSliderInput {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
border: 0 !important;
|
||||
|
|
@ -50,7 +50,7 @@ import {
|
|||
} from "../q-settings-constants";
|
||||
export { loader, action };
|
||||
|
||||
import "../q-settings.css";
|
||||
import styles from "./q.settings.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["q"],
|
||||
|
|
@ -153,7 +153,7 @@ function MapPicker() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<div>
|
||||
<span>{t("q:settings.maps.header")}</span> <MapIcon />
|
||||
</div>
|
||||
|
|
@ -257,7 +257,7 @@ function VoiceChat() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<div>
|
||||
<span>{t("q:settings.voiceChat.header")}</span>{" "}
|
||||
<MicrophoneFilledIcon />
|
||||
|
|
@ -388,7 +388,7 @@ function WeaponPool() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<div>
|
||||
<span>{t("q:settings.weaponPool.header")}</span> <PuzzleIcon />
|
||||
</div>
|
||||
|
|
@ -399,7 +399,7 @@ function WeaponPool() {
|
|||
name="weaponPool"
|
||||
value={JSON.stringify(weapons)}
|
||||
/>
|
||||
<div className="q-settings__weapon-pool-select-container">
|
||||
<div className={styles.weaponPoolSelectContainer}>
|
||||
{weapons.length < SENDOUQ_WEAPON_POOL_MAX_SIZE ? (
|
||||
<WeaponSelect
|
||||
onChange={(weaponSplId) => {
|
||||
|
|
@ -491,7 +491,7 @@ function Sounds() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<div>
|
||||
<span>{t("q:settings.sounds.header")}</span> <SpeakerFilledIcon />
|
||||
</div>
|
||||
|
|
@ -591,9 +591,9 @@ function SoundSlider() {
|
|||
|
||||
return (
|
||||
<div className="stack horizontal xs items-center ml-2-5">
|
||||
<SpeakerFilledIcon className="q-settings__volume-slider-icon" />
|
||||
<SpeakerFilledIcon className={styles.volumeSliderIcon} />
|
||||
<input
|
||||
className="q-settings__volume-slider-input"
|
||||
className={styles.volumeSliderInput}
|
||||
type="range"
|
||||
value={volume}
|
||||
onChange={changeVolume}
|
||||
|
|
@ -610,7 +610,7 @@ function TrustedUsers() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<span>{t("q:settings.trusted.header")}</span> <UsersIcon />
|
||||
</summary>
|
||||
<div className="mb-4">
|
||||
|
|
@ -684,7 +684,7 @@ function Misc() {
|
|||
|
||||
return (
|
||||
<details>
|
||||
<summary className="q-settings__summary">
|
||||
<summary className={styles.summary}>
|
||||
<div>{t("q:settings.misc.header")}</div>
|
||||
</summary>
|
||||
<fetcher.Form method="post" className="mb-4 ml-2-5 stack sm">
|
||||
|
|
|
|||
|
|
@ -51,12 +51,7 @@ export function MemberAdder({
|
|||
<div>
|
||||
<label htmlFor="invite">{t("q:looking.groups.adder.inviteLink")}</label>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={inviteLink}
|
||||
readOnly
|
||||
id="invite"
|
||||
/>
|
||||
<input type="text" value={inviteLink} readOnly id="invite" />
|
||||
<SendouButton
|
||||
variant={copySuccess ? "outlined-success" : "outlined"}
|
||||
onPress={() => copyToClipboard(inviteLink)}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import { FormMessage } from "~/components/FormMessage";
|
|||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Main, mainStyles } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { uploadImagePage } from "~/utils/urls";
|
||||
import { TEAM } from "../team-constants";
|
||||
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
||||
import "../team.css";
|
||||
import { TeamGoBackButton } from "~/features/team/components/TeamGoBackButton";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { uploadImagePage } from "~/utils/urls";
|
||||
import { action } from "../actions/t.$customUrl.edit.server";
|
||||
import { loader } from "../loaders/t.$customUrl.edit.server";
|
||||
import styles from "../team.module.css";
|
||||
import { TEAM } from "../team-constants";
|
||||
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
|
|
@ -37,7 +37,7 @@ export default function EditTeamPage() {
|
|||
return (
|
||||
<Main className="stack lg">
|
||||
<TeamGoBackButton />
|
||||
<div className="half-width">
|
||||
<div className={mainStyles.narrow}>
|
||||
{isTeamOwner({ team, user }) ? (
|
||||
<FormWithConfirm
|
||||
dialogHeading={t("team:deleteTeam.header", { teamName: team.name })}
|
||||
|
|
@ -83,7 +83,7 @@ function ImageUploadLinks() {
|
|||
return (
|
||||
<div>
|
||||
<Label>{t("team:forms.fields.uploadImages")}</Label>
|
||||
<ol className="team__image-links-list">
|
||||
<ol className={styles.imageLinksList}>
|
||||
<li>
|
||||
<Link
|
||||
to={uploadImagePage({
|
||||
|
|
@ -116,7 +116,7 @@ function ImageRemoveButtons() {
|
|||
return team.avatarUrl || team.bannerUrl ? (
|
||||
<div>
|
||||
<Label>{t("team:forms.fields.removeImages")}</Label>
|
||||
<ol className="team__image-links-list">
|
||||
<ol className={styles.imageLinksList}>
|
||||
{team.avatarUrl ? (
|
||||
<li>
|
||||
<FormWithConfirm
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ import { UsersIcon } from "~/components/icons/Users";
|
|||
import { Placement } from "~/components/Placement";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { TeamLoaderData } from "~/features/team/loaders/t.$customUrl.server";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { editTeamPage, manageTeamRosterPage, userPage } from "~/utils/urls";
|
||||
import { action } from "../actions/t.$customUrl.index.server";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
import styles from "../team.module.css";
|
||||
import {
|
||||
isTeamManager,
|
||||
isTeamMember,
|
||||
isTeamOwner,
|
||||
resolveNewOwner,
|
||||
} from "../team-utils";
|
||||
import "../team.css";
|
||||
import type { TeamLoaderData } from "~/features/team/loaders/t.$customUrl.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { action } from "../actions/t.$customUrl.index.server";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
export { action };
|
||||
|
||||
export default function TeamIndexPage() {
|
||||
|
|
@ -70,7 +70,7 @@ function ActionButtons() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="team__action-buttons">
|
||||
<div className={styles.actionButtons}>
|
||||
{isTeamMember({ user, team }) && !isMainTeam ? (
|
||||
<ChangeMainTeamButton />
|
||||
) : null}
|
||||
|
|
@ -150,9 +150,9 @@ function ResultsBanner({
|
|||
results: NonNullable<TeamLoaderData["results"]>;
|
||||
}) {
|
||||
return (
|
||||
<Link className="team__results" to="results">
|
||||
<Link className={styles.results} to="results">
|
||||
<div>View {results.count} results</div>
|
||||
<ul className="team__results__placements">
|
||||
<ul className={styles.resultsPlacements}>
|
||||
{results.placements.map(({ placement, count }) => {
|
||||
return (
|
||||
<li key={placement}>
|
||||
|
|
@ -176,23 +176,23 @@ function MemberRow({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="team__member"
|
||||
className={styles.member}
|
||||
data-testid={member.isOwner ? `member-owner-${member.id}` : undefined}
|
||||
>
|
||||
{member.role ? (
|
||||
<span
|
||||
className="team__member__role"
|
||||
className={styles.memberRole}
|
||||
data-testid={`member-row-role-${number}`}
|
||||
>
|
||||
{t(`team:roles.${member.role}`)}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="team__member__section">
|
||||
<div className={styles.memberSection}>
|
||||
<Link
|
||||
to={userPage(member)}
|
||||
className="team__member__avatar-name-container"
|
||||
className={styles.memberAvatarNameContainer}
|
||||
>
|
||||
<div className="team__member__avatar">
|
||||
<div className={styles.memberAvatar}>
|
||||
<Avatar user={member} size="md" />
|
||||
</div>
|
||||
{member.username}
|
||||
|
|
@ -221,11 +221,11 @@ function MobileMemberCard({
|
|||
const { t } = useTranslation(["team"]);
|
||||
|
||||
return (
|
||||
<div className="team__member-card__container">
|
||||
<div className="team__member-card">
|
||||
<div className={styles.memberCardContainer}>
|
||||
<div className={styles.memberCard}>
|
||||
<Link to={userPage(member)} className="stack items-center">
|
||||
<Avatar user={member} size="md" />
|
||||
<div className="team__member-card__name">{member.username}</div>
|
||||
<div className={styles.memberCardName}>{member.username}</div>
|
||||
</Link>
|
||||
{member.weapons.length > 0 ? (
|
||||
<div className="stack horizontal md">
|
||||
|
|
@ -242,7 +242,7 @@ function MobileMemberCard({
|
|||
) : null}
|
||||
</div>
|
||||
{member.role ? (
|
||||
<span className="team__member__role__mobile">
|
||||
<span className={styles.memberRoleMobile}>
|
||||
{t(`team:roles.${member.role}`)}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import { useTranslation } from "react-i18next";
|
|||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import "../team.css";
|
||||
|
||||
import { action } from "../actions/t.$customUrl.join.server";
|
||||
import { loader } from "../loaders/t.$customUrl.join.server";
|
||||
import styles from "../team.module.css";
|
||||
export { loader, action };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -28,7 +27,7 @@ export default function JoinTeamPage() {
|
|||
|
||||
return (
|
||||
<Main>
|
||||
<Form method="post" className="team__invite-container">
|
||||
<Form method="post" className={styles.inviteContainer}>
|
||||
<div className="text-center">
|
||||
{t(`team:validation.${validation}`, { teamName })}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,16 +13,15 @@ import { TrashIcon } from "~/components/icons/Trash";
|
|||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { joinTeamPage } from "~/utils/urls";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
import { TEAM_MEMBER_ROLES } from "../team-constants";
|
||||
import { isTeamFull } from "../team-utils";
|
||||
import "../team.css";
|
||||
import { TeamGoBackButton } from "~/features/team/components/TeamGoBackButton";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
import { joinTeamPage } from "~/utils/urls";
|
||||
import { action } from "../actions/t.$customUrl.roster.server";
|
||||
import { loader } from "../loaders/t.$customUrl.roster.server";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
import styles from "../team.module.css";
|
||||
import { TEAM_MEMBER_ROLES } from "../team-constants";
|
||||
import { isTeamFull } from "../team-utils";
|
||||
export { loader, action };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
|
|
@ -111,7 +110,7 @@ function MemberActions() {
|
|||
<div className="stack md">
|
||||
<h2 className="text-lg">{t("team:roster.members.header")}</h2>
|
||||
|
||||
<div className="team__roster__members">
|
||||
<div className={styles.rosterMembers}>
|
||||
{team.members.map((member, i) => (
|
||||
<MemberRow key={member.id} member={member} number={i} />
|
||||
))}
|
||||
|
|
@ -152,10 +151,7 @@ function MemberRow({
|
|||
|
||||
return (
|
||||
<React.Fragment key={member.id}>
|
||||
<div
|
||||
className="team__roster__members__member"
|
||||
data-testid={`member-row-${number}`}
|
||||
>
|
||||
<div className={styles.rosterMember} data-testid={`member-row-${number}`}>
|
||||
{member.username}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -229,7 +225,7 @@ function MemberRow({
|
|||
</SendouButton>
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
<hr className="team__roster__separator" />
|
||||
<hr className={styles.rosterSeparator} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { bskyUrl, navIconUrl, TEAM_SEARCH_PAGE, teamPage } from "~/utils/urls";
|
|||
import { loader } from "../loaders/t.$customUrl.server";
|
||||
export { loader };
|
||||
|
||||
import "../team.css";
|
||||
import styles from "../team.module.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
|
@ -72,9 +72,10 @@ function TeamBanner() {
|
|||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx("team__banner", {
|
||||
team__banner__placeholder: !team.bannerUrl,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.banner,
|
||||
!team.bannerUrl && styles.bannerPlaceholder,
|
||||
)}
|
||||
style={{
|
||||
"--team-banner-img": team.bannerUrl
|
||||
? `url("${team.bannerUrl}")`
|
||||
|
|
@ -82,13 +83,13 @@ function TeamBanner() {
|
|||
}}
|
||||
>
|
||||
{team.avatarUrl ? (
|
||||
<div className="team__banner__avatar">
|
||||
<div className={styles.bannerAvatar}>
|
||||
<div>
|
||||
<img src={team.avatarUrl} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="team__banner__flags">
|
||||
<div className={styles.bannerFlags}>
|
||||
{R.unique(
|
||||
team.members
|
||||
.map((member) => member.country)
|
||||
|
|
@ -97,16 +98,16 @@ function TeamBanner() {
|
|||
return <Flag key={country} countryCode={country} />;
|
||||
})}
|
||||
</div>
|
||||
<div className="team__banner__name">
|
||||
<div className={styles.bannerName}>
|
||||
{team.tag ? (
|
||||
<div className="team__banner__tag team__banner__tag__desktop">
|
||||
<div className={`${styles.bannerTag} ${styles.bannerTagDesktop}`}>
|
||||
{team.tag}
|
||||
</div>
|
||||
) : null}
|
||||
{team.name} <BskyLink />
|
||||
</div>
|
||||
</div>
|
||||
{team.avatarUrl ? <div className="team__banner__avatar__spacer" /> : null}
|
||||
{team.avatarUrl ? <div className={styles.bannerAvatarSpacer} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -115,7 +116,7 @@ function MobileTeamNameCountry() {
|
|||
const { team } = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="team__mobile-name-country">
|
||||
<div className={styles.mobileNameCountry}>
|
||||
<div className="stack horizontal sm">
|
||||
{R.unique(
|
||||
team.members
|
||||
|
|
@ -125,12 +126,12 @@ function MobileTeamNameCountry() {
|
|||
return <Flag key={country} countryCode={country} tiny />;
|
||||
})}
|
||||
</div>
|
||||
<div className="team__mobile-team-name">
|
||||
<div className={styles.mobileTeamName}>
|
||||
{team.name}
|
||||
<BskyLink />
|
||||
</div>
|
||||
{team.tag ? (
|
||||
<div className="team__banner__tag team__banner__tag__mobile">
|
||||
<div className={`${styles.bannerTag} ${styles.bannerTagMobile}`}>
|
||||
{team.tag}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -145,7 +146,7 @@ function BskyLink() {
|
|||
|
||||
return (
|
||||
<a
|
||||
className="team__bsky-link"
|
||||
className={styles.bskyLink}
|
||||
data-testid="bsky-link"
|
||||
href={bskyUrl(team.bsky)}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { loader } from "../loaders/t.server";
|
|||
import { TEAM, TEAMS_PER_PAGE } from "../team-constants";
|
||||
export { loader, action };
|
||||
|
||||
import "../team.css";
|
||||
import styles from "../team.module.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
|
|
@ -99,8 +99,8 @@ export default function TeamSearchPage() {
|
|||
<NewTeamDialog />
|
||||
<div className="stack sm horizontal justify-between">
|
||||
<Input
|
||||
className="team-search__input"
|
||||
icon={<SearchIcon className="team-search__icon" />}
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon className={styles.searchIcon} />}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder={t("team:teamSearch.placeholder")}
|
||||
|
|
@ -113,7 +113,7 @@ export default function TeamSearchPage() {
|
|||
<Link
|
||||
key={team.customUrl}
|
||||
to={teamPage(team.customUrl)}
|
||||
className="team-search__team"
|
||||
className={styles.searchTeam}
|
||||
>
|
||||
{team.avatarUrl ? (
|
||||
<img
|
||||
|
|
@ -125,21 +125,18 @@ export default function TeamSearchPage() {
|
|||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="team-search__team__avatar-placeholder">
|
||||
<div className={styles.searchTeamAvatarPlaceholder}>
|
||||
{team.name[0]}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div
|
||||
className="team-search__team__name"
|
||||
data-testid={`team-${i}`}
|
||||
>
|
||||
<div className={styles.searchTeamName} data-testid={`team-${i}`}>
|
||||
{team.name}
|
||||
{team.tag ? (
|
||||
<span className="team-search__team__tag">{team.tag}</span>
|
||||
<span className={styles.searchTeamTag}>{team.tag}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="team-search__team__members">
|
||||
<div className={styles.searchTeamMembers}>
|
||||
{team.members.length === 1
|
||||
? team.members[0].username
|
||||
: new Intl.ListFormat(i18n.language, {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
.team-search__input {
|
||||
.searchInput {
|
||||
height: 40px !important;
|
||||
font-size: var(--fonts-lg);
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.team-search__icon {
|
||||
.searchIcon {
|
||||
height: 25px;
|
||||
margin: auto;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.team-search__team {
|
||||
.searchTeam {
|
||||
display: flex;
|
||||
color: var(--color-text);
|
||||
gap: var(--s-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.team-search__team__name {
|
||||
.searchTeamName {
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--bold);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.team-search__team__members {
|
||||
.searchTeamMembers {
|
||||
font-size: var(--fonts-xs);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.team-search__team__tag {
|
||||
.searchTeamTag {
|
||||
font-size: var(--fonts-xs);
|
||||
color: var(--color-accent);
|
||||
padding: var(--s-0-5) var(--s-1);
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.team-search__team__avatar-placeholder {
|
||||
.searchTeamAvatarPlaceholder {
|
||||
height: 64px;
|
||||
min-width: 64px;
|
||||
display: grid;
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.team__banner {
|
||||
.banner {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
|
|
@ -64,19 +64,19 @@
|
|||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.team__banner__placeholder {
|
||||
.bannerPlaceholder {
|
||||
height: 6rem;
|
||||
background-color: var(--color-accent-low);
|
||||
}
|
||||
|
||||
.team__banner__flags {
|
||||
.bannerFlags {
|
||||
grid-area: flags;
|
||||
margin-top: -5px;
|
||||
display: none;
|
||||
column-gap: var(--s-4);
|
||||
}
|
||||
|
||||
.team__banner__name {
|
||||
.bannerName {
|
||||
grid-area: name;
|
||||
align-self: flex-end;
|
||||
justify-self: flex-end;
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.team__bsky-link {
|
||||
.bskyLink {
|
||||
padding: var(--s-1);
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
|
|
@ -102,21 +102,21 @@
|
|||
place-items: center;
|
||||
}
|
||||
|
||||
.team__bsky-link > svg {
|
||||
.bskyLink > svg {
|
||||
width: 0.9rem;
|
||||
}
|
||||
|
||||
.team__bsky-link path {
|
||||
.bskyLink path {
|
||||
fill: #1285fe;
|
||||
}
|
||||
|
||||
.team__banner__avatar {
|
||||
.bannerAvatar {
|
||||
grid-area: avatar;
|
||||
align-self: flex-end;
|
||||
margin-bottom: -110px;
|
||||
}
|
||||
|
||||
.team__banner__avatar > div {
|
||||
.bannerAvatar > div {
|
||||
padding: var(--s-2);
|
||||
background-color: var(--color-bg);
|
||||
border-radius: 100%;
|
||||
|
|
@ -125,11 +125,11 @@
|
|||
width: 7rem;
|
||||
}
|
||||
|
||||
.team__banner__avatar__spacer {
|
||||
.bannerAvatarSpacer {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.team__mobile-name-country {
|
||||
.mobileNameCountry {
|
||||
display: flex;
|
||||
font-size: var(--fonts-xl);
|
||||
align-items: center;
|
||||
|
|
@ -138,13 +138,13 @@
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.team__mobile-team-name {
|
||||
.mobileTeamName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.team__banner__tag {
|
||||
.bannerTag {
|
||||
font-size: var(--fonts-sm);
|
||||
background-color: var(--color-accent-low);
|
||||
color: var(--color-accent);
|
||||
|
|
@ -152,30 +152,30 @@
|
|||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.team__banner__tag__desktop {
|
||||
.bannerTagDesktop {
|
||||
position: absolute;
|
||||
bottom: 41px;
|
||||
right: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.team__banner__tag__mobile {
|
||||
.bannerTagMobile {
|
||||
font-size: var(--fonts-xs);
|
||||
padding: var(--s-0-5) var(--s-1);
|
||||
margin-block: var(--s-1);
|
||||
}
|
||||
|
||||
.team__banner__avatar img {
|
||||
.bannerAvatar img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.team__badges {
|
||||
.badges {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.team__badges > div {
|
||||
.badges > div {
|
||||
background-color: var(--color-accent-low);
|
||||
border-radius: var(--rounded);
|
||||
font-size: var(--fonts-xs);
|
||||
|
|
@ -187,14 +187,14 @@
|
|||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.team__action-buttons {
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.team__results {
|
||||
.results {
|
||||
background-color: var(--color-bg-higher);
|
||||
max-width: 32rem;
|
||||
margin: 0 auto;
|
||||
|
|
@ -209,24 +209,24 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team__results__placements {
|
||||
.resultsPlacements {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.team__results__placements > li {
|
||||
.resultsPlacements > li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.team__member {
|
||||
.member {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.team__member__section {
|
||||
.memberSection {
|
||||
background-color: var(--color-bg-high);
|
||||
border-radius: var(--rounded);
|
||||
padding: var(--s-2) var(--s-4);
|
||||
|
|
@ -238,7 +238,7 @@
|
|||
height: 4.5rem;
|
||||
}
|
||||
|
||||
.team__member__avatar-name-container {
|
||||
.memberAvatarNameContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-4);
|
||||
|
|
@ -246,13 +246,13 @@
|
|||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.team__member__avatar {
|
||||
.memberAvatar {
|
||||
background-color: var(--color-bg);
|
||||
padding: var(--s-2);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.team__member__role {
|
||||
.memberRole {
|
||||
margin-left: auto;
|
||||
font-size: var(--fonts-sm);
|
||||
color: var(--color-text-high);
|
||||
|
|
@ -260,12 +260,12 @@
|
|||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.team__member__role__mobile {
|
||||
.memberRoleMobile {
|
||||
color: var(--color-text-high);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.team__member-card__container {
|
||||
.memberCardContainer {
|
||||
width: 16rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
|
|
@ -273,7 +273,7 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.team__member-card {
|
||||
.memberCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -285,13 +285,13 @@
|
|||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.team__member-card__name {
|
||||
.memberCardName {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--bold);
|
||||
margin-block-start: var(--s-2);
|
||||
}
|
||||
|
||||
.team__roster__members {
|
||||
.rosterMembers {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--s-4);
|
||||
|
|
@ -299,18 +299,18 @@
|
|||
max-width: max-content;
|
||||
}
|
||||
|
||||
.team__roster__members__member {
|
||||
.rosterMember {
|
||||
justify-self: flex-start;
|
||||
font-weight: var(--bold);
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.team__roster__separator {
|
||||
.rosterSeparator {
|
||||
grid-column: 1 / 3;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.team__invite-container {
|
||||
.inviteContainer {
|
||||
margin-block-start: var(--s-14);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -318,7 +318,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.team__image-links-list {
|
||||
.imageLinksList {
|
||||
display: flex;
|
||||
gap: var(--s-8);
|
||||
padding-left: var(--s-4);
|
||||
|
|
@ -326,50 +326,50 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.team__banner__flags {
|
||||
.bannerFlags {
|
||||
display: flex;
|
||||
}
|
||||
.team__banner__name {
|
||||
.bannerName {
|
||||
display: flex;
|
||||
}
|
||||
.team__banner__tag__desktop {
|
||||
.bannerTagDesktop {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.team__banner__avatar > div {
|
||||
.bannerAvatar > div {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.team__banner__avatar {
|
||||
.bannerAvatar {
|
||||
margin-left: var(--s-2);
|
||||
margin-bottom: -90px;
|
||||
}
|
||||
|
||||
.team__mobile-name-country {
|
||||
.mobileNameCountry {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.team__member {
|
||||
.member {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.team__member-card__container {
|
||||
.memberCardContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.team__roster__members {
|
||||
.rosterMembers {
|
||||
grid-template-columns: 1fr 1fr max-content max-content;
|
||||
}
|
||||
|
||||
.team__roster__separator {
|
||||
.rosterSeparator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.team__banner__placeholder {
|
||||
.bannerPlaceholder {
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.team__action-buttons {
|
||||
.actionButtons {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
topSearchPage,
|
||||
topSearchPlayerPage,
|
||||
} from "~/utils/urls";
|
||||
import styles from "../top-search.module.css";
|
||||
import { monthYearToSpan } from "../top-search-utils";
|
||||
import type * as XRankPlacementRepository from "../XRankPlacementRepository.server";
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ export function PlacementsTable({
|
|||
const { t } = useTranslation(["game-misc"]);
|
||||
|
||||
return (
|
||||
<div className="placements__table">
|
||||
<div className={styles.table}>
|
||||
{placements.map((placement, i) => (
|
||||
<Link
|
||||
to={
|
||||
|
|
@ -34,14 +35,14 @@ export function PlacementsTable({
|
|||
: topSearchPlayerPage(placement.playerId)
|
||||
}
|
||||
key={placement.id}
|
||||
className="placements__table__row"
|
||||
className={styles.tableRow}
|
||||
data-testid={`placement-row-${i}`}
|
||||
>
|
||||
<div className="placements__table__inner-row">
|
||||
<div className="placements__table__rank">{placement.rank}</div>
|
||||
<div className={styles.tableInnerRow}>
|
||||
<div className={styles.tableRank}>{placement.rank}</div>
|
||||
{type === "MODE_INFO" ? (
|
||||
<>
|
||||
<div className="placements__table__mode">
|
||||
<div className={styles.tableMode}>
|
||||
<Image
|
||||
alt={
|
||||
placement.region === "WEST"
|
||||
|
|
@ -57,7 +58,7 @@ export function PlacementsTable({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="placements__table__mode">
|
||||
<div className={styles.tableMode}>
|
||||
<Image
|
||||
alt={t(`game-misc:MODE_LONG_${placement.mode}`)}
|
||||
path={modeImageUrl(placement.mode)}
|
||||
|
|
@ -67,7 +68,7 @@ export function PlacementsTable({
|
|||
</>
|
||||
) : null}
|
||||
<WeaponImage
|
||||
className="placements__table__weapon"
|
||||
className={styles.tableWeapon}
|
||||
variant="build"
|
||||
weaponSplId={placement.weaponSplId}
|
||||
width={32}
|
||||
|
|
@ -75,7 +76,7 @@ export function PlacementsTable({
|
|||
/>
|
||||
{type === "PLAYER_NAME" ? <div>{placement.name}</div> : null}
|
||||
{type === "MODE_INFO" ? (
|
||||
<div className="placements__time">
|
||||
<div className={styles.time}>
|
||||
{monthYearToSpan(placement).from.month}/
|
||||
{monthYearToSpan(placement).from.year} -{" "}
|
||||
{monthYearToSpan(placement).to.month}/
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import { PlacementsTable } from "../components/Placements";
|
|||
import { loader } from "../loaders/xsearch.player.$id.server";
|
||||
export { loader, action };
|
||||
|
||||
import "../top-search.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import { loader } from "../loaders/xsearch.server";
|
|||
import type { MonthYear } from "../top-search-utils";
|
||||
export { loader };
|
||||
|
||||
import "../top-search.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: () => ({
|
||||
imgPath: navIconUrl("xsearch"),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.placements__table {
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-0-5);
|
||||
|
|
@ -6,19 +6,19 @@
|
|||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.placements__table__rank {
|
||||
.tableRank {
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.placements__table__name {
|
||||
.tableName {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 117px;
|
||||
}
|
||||
|
||||
.placements__tier-header {
|
||||
.tierHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.placements__table__row {
|
||||
.tableRow {
|
||||
background-color: var(--color-bg-high);
|
||||
display: flex;
|
||||
padding: var(--s-2) var(--s-3);
|
||||
|
|
@ -36,23 +36,23 @@
|
|||
transition: 0.1s ease-in-out background-color;
|
||||
}
|
||||
|
||||
a.placements__table__row:hover {
|
||||
a.tableRow:hover {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.placements__table__row:first-of-type {
|
||||
.tableRow:first-of-type {
|
||||
border-radius: var(--rounded) var(--rounded) 0 0;
|
||||
}
|
||||
|
||||
.placements__table__row:last-of-type {
|
||||
.tableRow:last-of-type {
|
||||
border-radius: 0 0 var(--rounded) var(--rounded);
|
||||
}
|
||||
|
||||
.placements__table__row:only-child {
|
||||
.tableRow:only-child {
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.placements__table__row__qualification {
|
||||
.tableRowQualification {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
justify-content: center;
|
||||
|
|
@ -61,35 +61,35 @@ a.placements__table__row:hover {
|
|||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.placements__table__weapon {
|
||||
.tableWeapon {
|
||||
background-color: var(--color-bg);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.placements__table__mode {
|
||||
.tableMode {
|
||||
background-color: var(--color-bg);
|
||||
border-radius: 100%;
|
||||
padding: var(--s-1);
|
||||
}
|
||||
|
||||
.placements__time {
|
||||
.time {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.placements__table__power {
|
||||
.tablePower {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.placements__table__inner-row {
|
||||
.tableInnerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2-5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placements__avatar {
|
||||
.avatar {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import clsx from "clsx";
|
|||
import { TOURNAMENT } from "../../../tournament/tournament-constants";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import { getRounds } from "../../core/rounds";
|
||||
import styles from "./bracket.module.css";
|
||||
import { Match } from "./Match";
|
||||
import { RoundHeader } from "./RoundHeader";
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ export function EliminationBracketSide(props: EliminationBracketSideProps) {
|
|||
let atLeastOneColumnHidden = false;
|
||||
return (
|
||||
<div
|
||||
className="elim-bracket__container"
|
||||
className={styles.elimContainer}
|
||||
style={{ "--round-count": rounds.length }}
|
||||
>
|
||||
{rounds.flatMap((round, roundIdx) => {
|
||||
|
|
@ -48,7 +49,7 @@ export function EliminationBracketSide(props: EliminationBracketSideProps) {
|
|||
return (
|
||||
<div
|
||||
key={round.id}
|
||||
className="elim-bracket__round-column"
|
||||
className={styles.elimRoundColumn}
|
||||
data-round-id={round.id}
|
||||
>
|
||||
<RoundHeader
|
||||
|
|
@ -59,8 +60,8 @@ export function EliminationBracketSide(props: EliminationBracketSideProps) {
|
|||
maps={round.maps}
|
||||
/>
|
||||
<div
|
||||
className={clsx("elim-bracket__round-matches-container", {
|
||||
"elim-bracket__round-matches-container__top-bye":
|
||||
className={clsx(styles.elimRoundMatchesContainer, {
|
||||
[styles.elimRoundMatchesContainerTopBye]:
|
||||
!atLeastOneColumnHidden &&
|
||||
props.type === "winners" &&
|
||||
(!props.bracket.data.match[0].opponent1 ||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ import { tournamentMatchPage, tournamentStreamsPage } from "~/utils/urls";
|
|||
import type { Bracket } from "../../core/Bracket";
|
||||
import * as Deadline from "../../core/Deadline";
|
||||
import type { TournamentData } from "../../core/Tournament.server";
|
||||
import parentStyles from "../../tournament-bracket.module.css";
|
||||
import { matchEndedEarly } from "../../tournament-bracket-utils";
|
||||
import styles from "./bracket.module.css";
|
||||
|
||||
interface MatchProps {
|
||||
match: Unpacked<TournamentData["data"]["match"]>;
|
||||
|
|
@ -35,7 +37,7 @@ export function Match(props: MatchProps) {
|
|||
const isBye = !props.match.opponent1 || !props.match.opponent2;
|
||||
|
||||
if (isBye) {
|
||||
return <div className="bracket__match__bye" />;
|
||||
return <div className={styles.matchBye} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -43,7 +45,7 @@ export function Match(props: MatchProps) {
|
|||
<MatchHeader {...props} />
|
||||
<MatchWrapper {...props}>
|
||||
<MatchRow {...props} side={1} />
|
||||
<div className="bracket__match__separator" />
|
||||
<div className={styles.matchSeparator} />
|
||||
<MatchRow {...props} side={2} />
|
||||
</MatchWrapper>
|
||||
{!props.hideMatchTimer ? (
|
||||
|
|
@ -89,15 +91,20 @@ function MatchHeader({ match, type, roundNumber, group }: MatchProps) {
|
|||
tournament.ctx.castedMatchesInfo?.lockedMatches?.includes(match.id);
|
||||
|
||||
return (
|
||||
<div className="bracket__match__header">
|
||||
<div className="bracket__match__header__box">
|
||||
<div className={styles.matchHeader}>
|
||||
<div className={styles.matchHeaderBox}>
|
||||
{prefix()}
|
||||
{roundNumber}.{match.number}
|
||||
</div>
|
||||
{toBeCasted ? (
|
||||
<SendouPopover
|
||||
trigger={
|
||||
<SendouButton className="bracket__match__header__box bracket__match__header__box__button">
|
||||
<SendouButton
|
||||
className={clsx(
|
||||
styles.matchHeaderBox,
|
||||
styles.matchHeaderBoxButton,
|
||||
)}
|
||||
>
|
||||
🔒 CAST
|
||||
</SendouButton>
|
||||
}
|
||||
|
|
@ -109,7 +116,12 @@ function MatchHeader({ match, type, roundNumber, group }: MatchProps) {
|
|||
placement="top"
|
||||
popoverClassName="w-max"
|
||||
trigger={
|
||||
<SendouButton className="bracket__match__header__box bracket__match__header__box__button">
|
||||
<SendouButton
|
||||
className={clsx(
|
||||
styles.matchHeaderBox,
|
||||
styles.matchHeaderBoxButton,
|
||||
)}
|
||||
>
|
||||
🔴 LIVE
|
||||
</SendouButton>
|
||||
}
|
||||
|
|
@ -131,7 +143,7 @@ function MatchWrapper({
|
|||
if (!isPreview) {
|
||||
return (
|
||||
<Link
|
||||
className="bracket__match"
|
||||
className={styles.match}
|
||||
to={tournamentMatchPage({
|
||||
tournamentId: tournament.ctx.id,
|
||||
matchId: match.id,
|
||||
|
|
@ -143,7 +155,7 @@ function MatchWrapper({
|
|||
);
|
||||
}
|
||||
|
||||
return <div className="bracket__match">{children}</div>;
|
||||
return <div className={styles.match}>{children}</div>;
|
||||
}
|
||||
|
||||
function MatchRow({
|
||||
|
|
@ -214,30 +226,30 @@ function MatchRow({
|
|||
title={team?.members.map((m) => m.username).join(", ")}
|
||||
>
|
||||
<div
|
||||
className={clsx("bracket__match__seed", {
|
||||
className={clsx(styles.matchSeed, {
|
||||
"text-lighter-important italic opaque": simulated,
|
||||
bracket__match__seed__wide: isBigSeedNumber,
|
||||
[styles.matchSeedWide]: isBigSeedNumber,
|
||||
})}
|
||||
>
|
||||
{team?.seed}
|
||||
</div>
|
||||
{logoSrc ? <Avatar size="xxxs" url={logoSrc} className="mr-1" /> : null}
|
||||
<div
|
||||
className={clsx("bracket__match__team-name", {
|
||||
className={clsx(styles.matchTeamName, {
|
||||
"text-theme-secondary":
|
||||
!simulated && ownTeam && ownTeam?.id === team?.id,
|
||||
"text-lighter italic opaque": simulated,
|
||||
"bracket__match__team-name__narrow":
|
||||
[styles.matchTeamNameNarrow]:
|
||||
// either but not both
|
||||
(logoSrc || isBigSeedNumber) && !(logoSrc && isBigSeedNumber),
|
||||
// both
|
||||
"bracket__match__team-name__narrowest": logoSrc && isBigSeedNumber,
|
||||
[styles.matchTeamNameNarrowest]: logoSrc && isBigSeedNumber,
|
||||
invisible: !team,
|
||||
})}
|
||||
>
|
||||
{team?.name ?? "???"}
|
||||
</div>{" "}
|
||||
<div className="bracket__match__score">{score()}</div>
|
||||
<div className={styles.matchScore}>{score()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -253,7 +265,9 @@ function MatchStreams({ match }: Pick<MatchProps, "match">) {
|
|||
|
||||
if (!fetcher.data || !match.opponent1?.id || !match.opponent2?.id)
|
||||
return (
|
||||
<div className="text-lighter text-center tournament-bracket__stream-popover">
|
||||
<div
|
||||
className={clsx("text-lighter text-center", parentStyles.streamPopover)}
|
||||
>
|
||||
Loading streams...
|
||||
</div>
|
||||
);
|
||||
|
|
@ -274,7 +288,7 @@ function MatchStreams({ match }: Pick<MatchProps, "match">) {
|
|||
|
||||
if (streamsOfThisMatch.length === 0)
|
||||
return (
|
||||
<div className="tournament-bracket__stream-popover">
|
||||
<div className={parentStyles.streamPopover}>
|
||||
After all there seems to be no streams of this match. Check the{" "}
|
||||
<Link to={tournamentStreamsPage(tournament.ctx.id)}>streams page</Link>{" "}
|
||||
for all the available streams.
|
||||
|
|
@ -282,7 +296,9 @@ function MatchStreams({ match }: Pick<MatchProps, "match">) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="stack md justify-center tournament-bracket__stream-popover">
|
||||
<div
|
||||
className={clsx("stack md justify-center", parentStyles.streamPopover)}
|
||||
>
|
||||
{streamsOfThisMatch.map((stream) => (
|
||||
<TournamentStream
|
||||
key={stream.twitchUserName}
|
||||
|
|
@ -344,11 +360,8 @@ function MatchTimer({ match, bracket }: Pick<MatchProps, "match" | "bracket">) {
|
|||
: "var(--color-text)";
|
||||
|
||||
return (
|
||||
<div className="bracket__match__timer">
|
||||
<div
|
||||
className="bracket__match__header__box"
|
||||
style={{ color: statusColor }}
|
||||
>
|
||||
<div className={styles.matchTimer}>
|
||||
<div className={styles.matchHeaderBox} style={{ color: statusColor }}>
|
||||
{displayText}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { TOURNAMENT } from "../../../tournament/tournament-constants";
|
|||
import type { Bracket } from "../../core/Bracket";
|
||||
import * as Progression from "../../core/Progression";
|
||||
import * as Swiss from "../../core/Swiss";
|
||||
import styles from "./bracket.module.css";
|
||||
|
||||
export function PlacementsTable({
|
||||
groupId,
|
||||
|
|
@ -130,7 +131,7 @@ export function PlacementsTable({
|
|||
let eliminatedRowRendered = false;
|
||||
|
||||
return (
|
||||
<table className="rr__placements-table" cellSpacing={0}>
|
||||
<table className={styles.rrPlacementsTable} cellSpacing={0}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
|
|
@ -451,17 +452,17 @@ function SwissDividerRow({
|
|||
: `Eliminated (@ ${threshold} losses)`;
|
||||
|
||||
return (
|
||||
<tr className="tournament__standings__divider-row">
|
||||
<td colSpan={columnCount} className="tournament__standings__divider">
|
||||
<tr className={styles.standingsDividerRow}>
|
||||
<td colSpan={columnCount} className={styles.standingsDivider}>
|
||||
<div
|
||||
className={clsx("tournament__standings__divider-content", {
|
||||
"tournament__standings__divider--qualified": isQualified,
|
||||
"tournament__standings__divider--eliminated": !isQualified,
|
||||
className={clsx(styles.standingsDividerContent, {
|
||||
[styles.standingsDividerQualified]: isQualified,
|
||||
[styles.standingsDividerEliminated]: !isQualified,
|
||||
})}
|
||||
>
|
||||
<div className="tournament__standings__divider-line" />
|
||||
<span className="tournament__standings__divider-text">{message}</span>
|
||||
<div className="tournament__standings__divider-line" />
|
||||
<div className={styles.standingsDividerLine} />
|
||||
<span className={styles.standingsDividerText}>{message}</span>
|
||||
<div className={styles.standingsDividerLine} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from "clsx";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import * as React from "react";
|
||||
import type { TournamentRoundMaps } from "~/db/tables";
|
||||
|
|
@ -8,6 +9,7 @@ import { databaseTimestampToDate } from "~/utils/dates";
|
|||
import type { Unpacked } from "~/utils/types";
|
||||
import * as Deadline from "../../core/Deadline";
|
||||
import type { TournamentData } from "../../core/Tournament.server";
|
||||
import styles from "./bracket.module.css";
|
||||
|
||||
export function RoundHeader({
|
||||
roundId,
|
||||
|
|
@ -39,9 +41,9 @@ export function RoundHeader({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="elim-bracket__round-header">{name}</div>
|
||||
<div className={styles.elimRoundHeader}>{name}</div>
|
||||
{showInfos && bestOf && !leagueRoundStartDate ? (
|
||||
<div className="elim-bracket__round-header__infos">
|
||||
<div className={styles.elimRoundHeaderInfos}>
|
||||
<div>
|
||||
{countPrefix}
|
||||
{bestOf}
|
||||
|
|
@ -58,7 +60,7 @@ export function RoundHeader({
|
|||
) : leagueRoundStartDate ? (
|
||||
<LeagueRoundStartDate date={leagueRoundStartDate} />
|
||||
) : (
|
||||
<div className="elim-bracket__round-header__infos invisible">
|
||||
<div className={clsx(styles.elimRoundHeaderInfos, "invisible")}>
|
||||
Hidden
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -70,7 +72,7 @@ function LeagueRoundStartDate({ date }: { date: Date }) {
|
|||
const { formatDate } = useTimeFormat();
|
||||
|
||||
return (
|
||||
<div className="elim-bracket__round-header__infos">
|
||||
<div className={styles.elimRoundHeaderInfos}>
|
||||
<div>
|
||||
{formatDate(date, {
|
||||
month: "short",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import { groupNumberToLetters } from "../../tournament-bracket-utils";
|
||||
import styles from "./bracket.module.css";
|
||||
import { Match } from "./Match";
|
||||
import { PlacementsTable } from "./PlacementsTable";
|
||||
import { RoundHeader } from "./RoundHeader";
|
||||
|
|
@ -31,7 +32,7 @@ export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
|||
<div key={groupName} className="stack lg ml-6">
|
||||
<h2 className="text-lg">{groupName}</h2>
|
||||
<div
|
||||
className="elim-bracket__container"
|
||||
className={styles.elimContainer}
|
||||
style={{ "--round-count": rounds.length }}
|
||||
>
|
||||
{rounds.flatMap((round) => {
|
||||
|
|
@ -50,7 +51,7 @@ export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div key={round.id} className="elim-bracket__round-column">
|
||||
<div key={round.id} className={styles.elimRoundColumn}>
|
||||
<RoundHeader
|
||||
roundId={round.id}
|
||||
name={`Round ${round.number}`}
|
||||
|
|
@ -58,7 +59,7 @@ export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
|||
showInfos={someMatchOngoing}
|
||||
maps={round.maps}
|
||||
/>
|
||||
<div className="elim-bracket__round-matches-container">
|
||||
<div className={styles.elimRoundMatchesContainer}>
|
||||
{matches.map((match) => {
|
||||
if (!match.opponent1 || !match.opponent2) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import styles from "../../tournament-bracket.module.css";
|
||||
import { groupNumberToLetters } from "../../tournament-bracket-utils";
|
||||
import { Match } from "./Match";
|
||||
import { PlacementsTable } from "./PlacementsTable";
|
||||
|
|
@ -106,9 +107,10 @@ export function SwissBracket({
|
|||
key={g.groupId}
|
||||
onPress={() => setSelectedGroupId(g.groupId)}
|
||||
className={clsx(
|
||||
"tournament-bracket__bracket-nav__link tournament-bracket__bracket-nav__link__big",
|
||||
styles.bracketNavLink,
|
||||
styles.bracketNavLinkBig,
|
||||
{
|
||||
"tournament-bracket__bracket-nav__link__selected":
|
||||
[styles.bracketNavLinkSelected]:
|
||||
selectedGroupId === g.groupId,
|
||||
},
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
padding-block-end: var(--s-6);
|
||||
}
|
||||
|
||||
.scrolling-bracket {
|
||||
.scrollingBracket {
|
||||
padding: var(--s-4) var(--s-6);
|
||||
max-width: 100%;
|
||||
max-height: min(1000px, 70vh);
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
overflow: scroll;
|
||||
}
|
||||
|
||||
.bracket__match__header {
|
||||
.matchHeader {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
margin-block-start: -16px;
|
||||
}
|
||||
|
||||
.bracket__match__header__box {
|
||||
.matchHeaderBox {
|
||||
background-color: var(--color-bg-higher);
|
||||
padding: var(--s-0-5) var(--s-1);
|
||||
border-radius: var(--rounded-sm);
|
||||
|
|
@ -37,11 +37,11 @@
|
|||
border: 0;
|
||||
}
|
||||
|
||||
.bracket__match__header__box__button {
|
||||
.matchHeaderBoxButton {
|
||||
height: 18.86px;
|
||||
}
|
||||
|
||||
.bracket__match__timer {
|
||||
.matchTimer {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.bracket__match {
|
||||
.match {
|
||||
width: var(--match-width);
|
||||
min-height: var(--match-height);
|
||||
max-height: var(--match-height);
|
||||
|
|
@ -66,56 +66,56 @@
|
|||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
a.bracket__match:hover {
|
||||
a.match:hover {
|
||||
background-color: var(--color-bg-high);
|
||||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
.bracket__match__separator {
|
||||
.matchSeparator {
|
||||
min-height: 2px;
|
||||
max-height: 2px;
|
||||
width: 100%;
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.bracket__match__bye {
|
||||
.matchBye {
|
||||
visibility: hidden;
|
||||
min-height: var(--match-height);
|
||||
max-height: var(--match-height);
|
||||
}
|
||||
|
||||
.bracket__match__seed {
|
||||
.matchSeed {
|
||||
color: var(--color-text-accent);
|
||||
margin-inline-end: var(--s-0-5);
|
||||
min-width: 15px;
|
||||
max-width: 15px;
|
||||
}
|
||||
|
||||
.bracket__match__seed__wide {
|
||||
.matchSeedWide {
|
||||
min-width: 22px;
|
||||
max-width: 22px;
|
||||
}
|
||||
|
||||
.bracket__match__team-name {
|
||||
.matchTeamName {
|
||||
max-width: 95px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bracket__match__team-name__narrow {
|
||||
.matchTeamNameNarrow {
|
||||
max-width: 75px;
|
||||
}
|
||||
|
||||
.bracket__match__team-name__narrowest {
|
||||
.matchTeamNameNarrowest {
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
.bracket__match__score {
|
||||
.matchScore {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.elim-bracket__container {
|
||||
.elimContainer {
|
||||
--line-width: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
|
|
@ -125,7 +125,7 @@ a.bracket__match:hover {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.elim-bracket__round-matches-container {
|
||||
.elimRoundMatchesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
|
@ -135,11 +135,11 @@ a.bracket__match:hover {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.elim-bracket__round-matches-container__top-bye {
|
||||
.elimRoundMatchesContainerTopBye {
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.elim-bracket__round-header {
|
||||
.elimRoundHeader {
|
||||
text-align: center;
|
||||
background-color: var(--color-bg-higher);
|
||||
font-size: var(--fonts-xs);
|
||||
|
|
@ -149,7 +149,7 @@ a.bracket__match:hover {
|
|||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
.elim-bracket__round-header__infos {
|
||||
.elimRoundHeaderInfos {
|
||||
width: var(--match-width);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -158,12 +158,12 @@ a.bracket__match:hover {
|
|||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.elim-bracket__round-column {
|
||||
.elimRoundColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rr__placements-table {
|
||||
.rrPlacementsTable {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
min-width: max-content;
|
||||
|
|
@ -171,26 +171,69 @@ a.bracket__match:hover {
|
|||
max-width: 600px;
|
||||
}
|
||||
|
||||
.rr__placements-table thead {
|
||||
.rrPlacementsTable thead {
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.rr__placements-table th {
|
||||
.rrPlacementsTable th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rr__placements-table th abbr {
|
||||
.rrPlacementsTable th abbr {
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.rr__placements-table td span {
|
||||
.rrPlacementsTable td span {
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.rr__placements-table tbody tr:nth-child(odd) {
|
||||
.rrPlacementsTable tbody tr:nth-child(odd) {
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.rr__placements-table tbody tr:nth-child(even) {
|
||||
.rrPlacementsTable tbody tr:nth-child(even) {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.standingsDividerRow {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.standingsDivider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.standingsDividerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
padding-block: var(--s-2);
|
||||
}
|
||||
|
||||
.standingsDividerQualified .standingsDividerLine {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.standingsDividerQualified .standingsDividerText {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.standingsDividerEliminated .standingsDividerLine {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.standingsDividerEliminated .standingsDividerText {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.standingsDividerLine {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.standingsDividerText {
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useDraggable } from "react-use-draggable-scroll";
|
||||
import { useBracketExpanded } from "~/features/tournament/routes/to.$id";
|
||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||
import styles from "./bracket.module.css";
|
||||
import { EliminationBracketSide } from "./Elimination";
|
||||
import { RoundRobinBracket } from "./RoundRobin";
|
||||
import { SwissBracket } from "./Swiss";
|
||||
|
|
@ -68,7 +70,7 @@ function BracketContainer({
|
|||
}) {
|
||||
if (!scrollable) {
|
||||
return (
|
||||
<div className="bracket" data-testid="brackets-viewer">
|
||||
<div className={styles.bracket} data-testid="brackets-viewer">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -91,7 +93,7 @@ function ScrollableBracketContainer({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="bracket scrolling-bracket"
|
||||
className={clsx(styles.bracket, styles.scrollingBracket)}
|
||||
data-testid="brackets-viewer"
|
||||
ref={ref}
|
||||
{...events}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SubmitButton } from "~/components/SubmitButton";
|
|||
import { TournamentMatchStatus } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
const lockingInfo =
|
||||
"You can lock the match to indicate that it should not be started before the cast is ready. Match being locked prevents score reporting and hides the map list till the organizer/streamer unlocks it.";
|
||||
|
|
@ -106,19 +107,12 @@ function CastInfoWrapper({
|
|||
|
||||
return (
|
||||
<div className="stack horizontal sm justify-center items-center">
|
||||
<fetcher.Form
|
||||
className="tournament-bracket__cast-info-container"
|
||||
method="post"
|
||||
>
|
||||
<div className="tournament-bracket__cast-info-container__label">
|
||||
Cast
|
||||
</div>
|
||||
<fetcher.Form className={styles.castInfoContainer} method="post">
|
||||
<div className={styles.castInfoContainerLabel}>Cast</div>
|
||||
|
||||
<div className="stack horizontal sm items-center justify-between w-full">
|
||||
{children ? (
|
||||
<div className="tournament-bracket__cast-info-container__content">
|
||||
{children}
|
||||
</div>
|
||||
<div className={styles.castInfoContainerContent}>{children}</div>
|
||||
) : null}
|
||||
{submitButtonText && _action ? (
|
||||
<SubmitButton
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import clsx from "clsx";
|
||||
import { differenceInSeconds } from "date-fns";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { InfoPopover } from "~/components/InfoPopover";
|
||||
import * as Deadline from "../core/Deadline";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
interface DeadlineInfoPopoverProps {
|
||||
startedAt: Date;
|
||||
|
|
@ -36,21 +38,28 @@ export function DeadlineInfoPopover({
|
|||
|
||||
const warningIndicator =
|
||||
status === "warning" ? (
|
||||
<span className="tournament-bracket__deadline-indicator tournament-bracket__deadline-indicator__warning">
|
||||
<span
|
||||
className={clsx(
|
||||
styles.deadlineIndicator,
|
||||
styles.deadlineIndicatorWarning,
|
||||
)}
|
||||
>
|
||||
!
|
||||
</span>
|
||||
) : status === "error" ? (
|
||||
<span className="tournament-bracket__deadline-indicator tournament-bracket__deadline-indicator__error">
|
||||
<span
|
||||
className={clsx(
|
||||
styles.deadlineIndicator,
|
||||
styles.deadlineIndicatorError,
|
||||
)}
|
||||
>
|
||||
!
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__deadline-popover">
|
||||
<InfoPopover
|
||||
tiny
|
||||
className="tournament-bracket__deadline-popover__trigger"
|
||||
>
|
||||
<div className={styles.deadlinePopover}>
|
||||
<InfoPopover tiny className={styles.deadlinePopoverTrigger}>
|
||||
{t("tournament:match.deadline.explanation")}
|
||||
</InfoPopover>
|
||||
{warningIndicator}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import invariant from "~/utils/invariant";
|
|||
import * as PickBan from "../core/PickBan";
|
||||
import type { TournamentDataTeam } from "../core/Tournament.server";
|
||||
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
import {
|
||||
isSetOverByScore,
|
||||
matchIsLocked,
|
||||
|
|
@ -140,10 +141,7 @@ export function MatchActions({
|
|||
revising={revising}
|
||||
/>
|
||||
{!presentational && bothTeamsHaveActiveRosters ? (
|
||||
<Form
|
||||
method="post"
|
||||
className="tournament-bracket__during-match-actions__actions"
|
||||
>
|
||||
<Form method="post" className={styles.duringMatchActionsActions}>
|
||||
<input type="hidden" name="winnerTeamId" value={winnerId ?? ""} />
|
||||
{showPoints ? (
|
||||
<input type="hidden" name="points" value={JSON.stringify(points)} />
|
||||
|
|
@ -180,8 +178,8 @@ export function MatchActions({
|
|||
/>
|
||||
) : null}
|
||||
{!result && presentational ? (
|
||||
<div className="tournament-bracket__during-match-actions__actions">
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<div className={styles.duringMatchActionsActions}>
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
No permissions to report score
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -238,7 +236,7 @@ function ReportScoreButtons({
|
|||
);
|
||||
if (leagueRoundStartDate && leagueRoundStartDate > new Date()) {
|
||||
return (
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
League round has not started yet
|
||||
</p>
|
||||
);
|
||||
|
|
@ -246,7 +244,7 @@ function ReportScoreButtons({
|
|||
|
||||
if (matchLocked) {
|
||||
return (
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
Match is pending to be casted. Please wait a bit
|
||||
</p>
|
||||
);
|
||||
|
|
@ -258,7 +256,7 @@ function ReportScoreButtons({
|
|||
points[winnerIdx] <= points[winnerIdx === 0 ? 1 : 0]
|
||||
) {
|
||||
return (
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
Winner should have higher score than loser
|
||||
</p>
|
||||
);
|
||||
|
|
@ -270,7 +268,7 @@ function ReportScoreButtons({
|
|||
(points[0] !== 0 && points[1] === 100))
|
||||
) {
|
||||
return (
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
If there was a KO (100 score), other team should have 0 score
|
||||
</p>
|
||||
);
|
||||
|
|
@ -278,7 +276,7 @@ function ReportScoreButtons({
|
|||
|
||||
if (typeof winnerIdx !== "number") {
|
||||
return (
|
||||
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
|
||||
<p className={styles.duringMatchActionsAmountWarningParagraph}>
|
||||
Please select the winner of this map
|
||||
</p>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Avatar } from "~/components/Avatar";
|
|||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import { tournamentTeamPage, userPage } from "~/utils/urls";
|
||||
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
const INACTIVE_PLAYER_CSS =
|
||||
"tournament__team-with-roster__member__inactive text-lighter-important";
|
||||
|
|
@ -39,17 +40,16 @@ export function MatchRosters({
|
|||
: null;
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__rosters">
|
||||
<div className={styles.rosters}>
|
||||
<div className="stack xxs">
|
||||
<div className="stack xs horizontal items-center text-lighter">
|
||||
<div className="tournament-bracket__team-one-dot" />
|
||||
<div className={styles.teamOneDot} />
|
||||
Team 1
|
||||
</div>
|
||||
<h2
|
||||
className={clsx("text-sm", {
|
||||
"text-lighter": !teamOne,
|
||||
"tournament-bracket__rosters__spaced-header":
|
||||
teamOneLogoSrc || teamTwoLogoSrc,
|
||||
[styles.rostersSpacedHeader]: teamOneLogoSrc || teamTwoLogoSrc,
|
||||
})}
|
||||
>
|
||||
{teamOne ? (
|
||||
|
|
@ -96,14 +96,13 @@ export function MatchRosters({
|
|||
</div>
|
||||
<div className="stack xxs">
|
||||
<div className="stack xs horizontal items-center text-lighter">
|
||||
<div className="tournament-bracket__team-two-dot" />
|
||||
<div className={styles.teamTwoDot} />
|
||||
Team 2
|
||||
</div>
|
||||
<h2
|
||||
className={clsx("text-sm", {
|
||||
"text-lighter": !teamTwo,
|
||||
"tournament-bracket__rosters__spaced-header":
|
||||
teamOneLogoSrc || teamTwoLogoSrc,
|
||||
[styles.rostersSpacedHeader]: teamOneLogoSrc || teamTwoLogoSrc,
|
||||
})}
|
||||
>
|
||||
{teamTwo ? (
|
||||
|
|
|
|||
|
|
@ -37,11 +37,13 @@ import {
|
|||
specialWeaponImageUrl,
|
||||
stageImageUrl,
|
||||
} from "~/utils/urls";
|
||||
import tournamentStyles from "../../tournament/tournament.module.css";
|
||||
import type { Bracket } from "../core/Bracket";
|
||||
import * as Deadline from "../core/Deadline";
|
||||
import * as PickBan from "../core/PickBan";
|
||||
import type { TournamentDataTeam } from "../core/Tournament.server";
|
||||
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
import {
|
||||
groupNumberToLetters,
|
||||
mapCountPlayedInSetWithCertainty,
|
||||
|
|
@ -168,7 +170,7 @@ export function StartedMatch({
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__during-match-actions">
|
||||
<div className={styles.duringMatchActions}>
|
||||
<FancyStageBanner
|
||||
stage={currentStageWithMode}
|
||||
infos={roundInfos}
|
||||
|
|
@ -190,10 +192,10 @@ export function StartedMatch({
|
|||
name="position"
|
||||
value={currentPosition - 1}
|
||||
/>
|
||||
<div className="tournament-bracket__stage-banner__bottom-bar">
|
||||
<div className={styles.stageBannerBottomBar}>
|
||||
<SubmitButton
|
||||
_action="UNDO_REPORT_SCORE"
|
||||
className="tournament-bracket__stage-banner__undo-button"
|
||||
className={styles.stageBannerUndoButton}
|
||||
testId="undo-score-button"
|
||||
>
|
||||
{t("tournament:match.action.undoLastScore")}
|
||||
|
|
@ -205,10 +207,10 @@ export function StartedMatch({
|
|||
tournament.matchCanBeReopened(data.match.id) &&
|
||||
presentational && (
|
||||
<Form method="post">
|
||||
<div className="tournament-bracket__stage-banner__bottom-bar">
|
||||
<div className={styles.stageBannerBottomBar}>
|
||||
<SubmitButton
|
||||
_action="REOPEN_MATCH"
|
||||
className="tournament-bracket__stage-banner__undo-button"
|
||||
className={styles.stageBannerUndoButton}
|
||||
testId="reopen-match-button"
|
||||
>
|
||||
{t("tournament:match.action.reopenMatch")}
|
||||
|
|
@ -354,14 +356,14 @@ function FancyStageBanner({
|
|||
return (
|
||||
<>
|
||||
{inBanPhase ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">Banning phase</div>
|
||||
<div>Waiting for {banPickingTeam()?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : !stage ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">Counterpick</div>
|
||||
<div>Waiting for {banPickingTeam()?.name}</div>
|
||||
|
|
@ -369,7 +371,7 @@ function FancyStageBanner({
|
|||
</div>
|
||||
</div>
|
||||
) : matchIsLocked ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">
|
||||
Match locked to be casted
|
||||
|
|
@ -378,7 +380,7 @@ function FancyStageBanner({
|
|||
</div>
|
||||
</div>
|
||||
) : waitingForLeagueRoundToStart ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">
|
||||
Waiting for league round to start
|
||||
|
|
@ -394,7 +396,7 @@ function FancyStageBanner({
|
|||
</div>
|
||||
</div>
|
||||
) : waitingForPreviousMatch ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">
|
||||
Previous match ongoing
|
||||
|
|
@ -405,7 +407,7 @@ function FancyStageBanner({
|
|||
</div>
|
||||
</div>
|
||||
) : waitingForActiveRosterSelectionFor ? (
|
||||
<div className="tournament-bracket__locked-banner">
|
||||
<div className={styles.lockedBanner}>
|
||||
<div className="stack sm items-center">
|
||||
<div
|
||||
className="text-lg text-center font-bold"
|
||||
|
|
@ -432,25 +434,20 @@ function FancyStageBanner({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx("tournament-bracket__stage-banner", {
|
||||
className={clsx(styles.stageBanner, {
|
||||
rounded: !infos,
|
||||
})}
|
||||
style={style}
|
||||
data-testid="stage-banner"
|
||||
>
|
||||
<div className="tournament-bracket__stage-banner__top-bar">
|
||||
<h4 className="tournament-bracket__stage-banner__top-bar__header">
|
||||
<Image
|
||||
className="tournament-bracket__stage-banner__top-bar__mode-image"
|
||||
path={modeImageUrl(stage.mode)}
|
||||
alt=""
|
||||
width={24}
|
||||
/>
|
||||
<span className="tournament-bracket__stage-banner__top-bar__map-text-small">
|
||||
<div className={styles.stageBannerTopBar}>
|
||||
<h4 className={styles.stageBannerTopBarHeader}>
|
||||
<Image path={modeImageUrl(stage.mode)} alt="" width={24} />
|
||||
<span className={styles.stageBannerTopBarMapTextSmall}>
|
||||
{t(`game-misc:MODE_SHORT_${stage.mode}`)}{" "}
|
||||
{t(`game-misc:STAGE_${stage.stageId}`)}
|
||||
</span>
|
||||
<span className="tournament-bracket__stage-banner__top-bar__map-text-big">
|
||||
<span className={styles.stageBannerTopBarMapTextBig}>
|
||||
{t(`game-misc:MODE_LONG_${stage.mode}`)} on{" "}
|
||||
{t(`game-misc:STAGE_${stage.stageId}`)}
|
||||
</span>
|
||||
|
|
@ -485,7 +482,7 @@ function FancyStageBanner({
|
|||
/>
|
||||
) : null}
|
||||
{infos && (
|
||||
<div className="tournament-bracket__infos">
|
||||
<div className={styles.infos}>
|
||||
{infos.filter(Boolean).map((info, i) => (
|
||||
<div key={i}>{info}</div>
|
||||
))}
|
||||
|
|
@ -535,8 +532,8 @@ function ModeProgressIndicator({
|
|||
|
||||
// TODO: this should be button when we click on it
|
||||
return (
|
||||
<div className="tournament-bracket__mode-progress">
|
||||
<div className="tournament-bracket__mode-progress__inner">
|
||||
<div className={styles.modeProgress}>
|
||||
<div className={styles.modeProgressInner}>
|
||||
{nullFilledArray(
|
||||
Math.max(data.mapList?.length ?? 0, data.match.roundMaps?.count ?? 0),
|
||||
).map((_, i) => {
|
||||
|
|
@ -554,7 +551,7 @@ function ModeProgressIndicator({
|
|||
|
||||
if (!map?.mode) {
|
||||
return (
|
||||
<div key={i} className="tournament-bracket__mode-progress__image">
|
||||
<div key={i} className={styles.modeProgressImage}>
|
||||
<PickIcon />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -572,10 +569,13 @@ function ModeProgressIndicator({
|
|||
<SendouButton
|
||||
variant="minimal"
|
||||
size="small"
|
||||
className="tournament-bracket__mode-progress__image__banned__popover-trigger"
|
||||
className={styles.modeProgressImageBannedPopoverTrigger}
|
||||
>
|
||||
<Image
|
||||
containerClassName="tournament-bracket__mode-progress__image tournament-bracket__mode-progress__image__banned"
|
||||
containerClassName={clsx(
|
||||
styles.modeProgressImage,
|
||||
styles.modeProgressImageBanned,
|
||||
)}
|
||||
path={modeImageUrl(map.mode)}
|
||||
height={20}
|
||||
width={20}
|
||||
|
|
@ -597,24 +597,21 @@ function ModeProgressIndicator({
|
|||
|
||||
return (
|
||||
<Image
|
||||
containerClassName={clsx(
|
||||
"tournament-bracket__mode-progress__image",
|
||||
{
|
||||
"tournament-bracket__mode-progress__image__notable":
|
||||
adjustedI <= maxIndexThatWillBePlayedForSure,
|
||||
"tournament-bracket__mode-progress__image__team-one-win":
|
||||
data.results[adjustedI] &&
|
||||
data.results[adjustedI].winnerTeamId ===
|
||||
data.match.opponentOne?.id,
|
||||
"tournament-bracket__mode-progress__image__team-two-win":
|
||||
data.results[adjustedI] &&
|
||||
data.results[adjustedI].winnerTeamId ===
|
||||
data.match.opponentTwo?.id,
|
||||
"tournament-bracket__mode-progress__image__selected":
|
||||
adjustedI === selectedResultIndex,
|
||||
"cursor-pointer": Boolean(setSelectedResultIndex),
|
||||
},
|
||||
)}
|
||||
containerClassName={clsx(styles.modeProgressImage, {
|
||||
[styles.modeProgressImageNotable]:
|
||||
adjustedI <= maxIndexThatWillBePlayedForSure,
|
||||
[styles.modeProgressImageTeamOneWin]:
|
||||
data.results[adjustedI] &&
|
||||
data.results[adjustedI].winnerTeamId ===
|
||||
data.match.opponentOne?.id,
|
||||
[styles.modeProgressImageTeamTwoWin]:
|
||||
data.results[adjustedI] &&
|
||||
data.results[adjustedI].winnerTeamId ===
|
||||
data.match.opponentTwo?.id,
|
||||
[styles.modeProgressImageSelected]:
|
||||
adjustedI === selectedResultIndex,
|
||||
"cursor-pointer": Boolean(setSelectedResultIndex),
|
||||
})}
|
||||
key={i}
|
||||
path={modeImageUrl(map.mode)}
|
||||
height={20}
|
||||
|
|
@ -766,8 +763,11 @@ function StartedMatchTabs({
|
|||
<Chat
|
||||
rooms={rooms}
|
||||
users={chatUsers}
|
||||
className="tournament__chat-container"
|
||||
messagesContainerClassName="tournament__chat-messages-container pt-0"
|
||||
className={tournamentStyles.chatContainer}
|
||||
messagesContainerClassName={clsx(
|
||||
tournamentStyles.chatMessagesContainer,
|
||||
"pt-0",
|
||||
)}
|
||||
chat={chat}
|
||||
onMount={onChatMount}
|
||||
onUnmount={onChatUnmount}
|
||||
|
|
@ -822,9 +822,9 @@ function ActionSectionWrapper({
|
|||
}
|
||||
: undefined;
|
||||
return (
|
||||
<section className="tournament__action-section" style={style}>
|
||||
<section className={tournamentStyles.actionSection} style={style}>
|
||||
<div
|
||||
className={clsx("tournament__action-section__content", {
|
||||
className={clsx({
|
||||
"justify-center": rest["justify-center"],
|
||||
})}
|
||||
>
|
||||
|
|
@ -839,8 +839,8 @@ function ScreenBanIcons({ banned }: { banned: boolean }) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx("tournament-bracket__no-screen", {
|
||||
"tournament-bracket__no-screen__banned": banned,
|
||||
className={clsx(styles.noScreen, {
|
||||
[styles.noScreenBanned]: banned,
|
||||
})}
|
||||
data-testid={`screen-${banned ? "banned" : "allowed"}`}
|
||||
>
|
||||
|
|
@ -871,7 +871,10 @@ function EndSetPopover({
|
|||
trigger={
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
className="tournament-bracket__stage-banner__undo-button tournament-bracket__stage-banner__end-set-button"
|
||||
className={clsx(
|
||||
styles.stageBannerUndoButton,
|
||||
styles.stageBannerEndSetButton,
|
||||
)}
|
||||
>
|
||||
{t("tournament:match.action.endSet")}
|
||||
</SendouButton>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { tournamentTeamPage, userPage } from "~/utils/urls";
|
|||
import { useTournament } from "../../tournament/routes/to.$id";
|
||||
import type { TournamentDataTeam } from "../core/Tournament.server";
|
||||
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
import { tournamentTeamToActiveRosterUserIds } from "../tournament-bracket-utils";
|
||||
import type { Result } from "./StartedMatch";
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ export function TeamRosterInputs({
|
|||
: _points;
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__during-match-actions__rosters">
|
||||
<div className={styles.duringMatchActionsRosters}>
|
||||
{teams.map((team, teamI) => {
|
||||
const winnerRadioChecked = result
|
||||
? result.winnerTeamId === team.id
|
||||
|
|
@ -253,27 +254,19 @@ function TeamRosterHeader({
|
|||
return (
|
||||
<>
|
||||
<div className="text-xs text-lighter font-semi-bold stack horizontal xs items-center justify-center">
|
||||
<div
|
||||
className={
|
||||
idx === 0
|
||||
? "tournament-bracket__team-one-dot"
|
||||
: "tournament-bracket__team-two-dot"
|
||||
}
|
||||
/>
|
||||
<div className={idx === 0 ? styles.teamOneDot : styles.teamTwoDot} />
|
||||
Team {idx + 1}
|
||||
</div>
|
||||
<h4>
|
||||
{team.seed ? (
|
||||
<span className="tournament-bracket__during-match-actions__seed">
|
||||
#{team.seed}
|
||||
</span>
|
||||
<span className={styles.duringMatchActionsSeed}>#{team.seed}</span>
|
||||
) : null}{" "}
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId,
|
||||
tournamentTeamId: team.id,
|
||||
})}
|
||||
className="tournament-bracket__during-match-actions__team-name"
|
||||
className={styles.duringMatchActionsTeamName}
|
||||
>
|
||||
{team.name}
|
||||
</Link>
|
||||
|
|
@ -316,12 +309,9 @@ function WinnerRadio({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"tournament-bracket__during-match-actions__radio-container",
|
||||
{
|
||||
invisible,
|
||||
},
|
||||
)}
|
||||
className={clsx(styles.duringMatchActionsRadioContainer, {
|
||||
invisible,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
@ -364,7 +354,7 @@ function PointInput({
|
|||
return (
|
||||
<div className="stack horizontal sm items-center">
|
||||
<input
|
||||
className="tournament-bracket__points-input"
|
||||
className={styles.pointsInput}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
type="number"
|
||||
min={0}
|
||||
|
|
@ -423,24 +413,21 @@ function TeamRosterInputsCheckboxes({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__during-match-actions__team-players">
|
||||
<div className={styles.duringMatchActionsTeamPlayers}>
|
||||
{members.map((member, i) => {
|
||||
return (
|
||||
<div className="stack horizontal xs" key={member.id}>
|
||||
<div
|
||||
className={clsx(
|
||||
"tournament-bracket__during-match-actions__checkbox-name",
|
||||
styles.duringMatchActionsCheckboxName,
|
||||
{ "disabled-opaque": mode() === "DISABLED" },
|
||||
{ presentational: mode() === "PRESENTATIONAL" },
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className={clsx(
|
||||
"tournament-bracket__during-match-actions__checkbox",
|
||||
{
|
||||
opaque: presentational,
|
||||
},
|
||||
)}
|
||||
className={clsx(styles.duringMatchActionsCheckbox, {
|
||||
opaque: presentational,
|
||||
})}
|
||||
type="checkbox"
|
||||
id={`${member.id}-${id}`}
|
||||
name="playerName"
|
||||
|
|
@ -451,10 +438,10 @@ function TeamRosterInputsCheckboxes({
|
|||
data-testid={`player-checkbox-${i}`}
|
||||
/>{" "}
|
||||
<label
|
||||
className="tournament-bracket__during-match-actions__player-name"
|
||||
className={styles.duringMatchActionsPlayerName}
|
||||
htmlFor={`${member.id}-${id}`}
|
||||
>
|
||||
<span className="tournament-bracket__during-match-actions__player-name__inner">
|
||||
<span className={styles.duringMatchActionsPlayerNameInner}>
|
||||
{member.inGameName
|
||||
? inGameNameWithoutDiscriminator(member.inGameName)
|
||||
: member.username}
|
||||
|
|
@ -490,11 +477,11 @@ function RosterFormWithButtons({
|
|||
|
||||
if (!editingRoster) {
|
||||
return (
|
||||
<div className="tournament-bracket__roster-buttons__container">
|
||||
<div className={styles.rosterButtonsContainer}>
|
||||
<SendouButton
|
||||
size="small"
|
||||
onPress={() => setEditingRoster(true)}
|
||||
className="tournament-bracket__edit-roster-button"
|
||||
className={styles.editRosterButton}
|
||||
variant="minimal"
|
||||
data-testid="edit-active-roster-button"
|
||||
>
|
||||
|
|
@ -505,10 +492,7 @@ function RosterFormWithButtons({
|
|||
}
|
||||
|
||||
return (
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
className="tournament-bracket__roster-buttons__container"
|
||||
>
|
||||
<fetcher.Form method="post" className={styles.rosterButtonsContainer}>
|
||||
<input
|
||||
type="hidden"
|
||||
name="roster"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
tournamentMatchPage,
|
||||
tournamentRegisterPage,
|
||||
} from "~/utils/urls";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
export function TournamentTeamActions() {
|
||||
const tournament = useTournament();
|
||||
|
|
@ -151,7 +152,7 @@ export function TournamentTeamActions() {
|
|||
if (status.type === "WAITING_FOR_BRACKET") {
|
||||
return (
|
||||
<Container spaced>
|
||||
<CheckmarkIcon className="tournament-bracket__quick-action__checkmark" />{" "}
|
||||
<CheckmarkIcon className={styles.quickActionCheckmark} />{" "}
|
||||
<div>
|
||||
Checked in, waiting on bracket
|
||||
<Dots />
|
||||
|
|
@ -177,9 +178,9 @@ function Container({
|
|||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx("tournament-bracket__quick-action", {
|
||||
"tournament-bracket__quick-action__spaced": spaced,
|
||||
"tournament-bracket__quick-action__very-spaced": spaced === "very",
|
||||
className={clsx(styles.quickAction, {
|
||||
[styles.quickActionSpaced]: spaced,
|
||||
[styles.quickActionVerySpaced]: spaced === "very",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,7 @@ import type { Bracket as BracketType } from "../core/Bracket";
|
|||
import * as PreparedMaps from "../core/PreparedMaps";
|
||||
export { action };
|
||||
|
||||
import "../tournament-bracket.css";
|
||||
import "../components/Bracket/bracket.css";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
export default function TournamentBracketsPage() {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
|
|
@ -175,7 +174,7 @@ export default function TournamentBracketsPage() {
|
|||
<div>
|
||||
<Outlet context={ctx} />
|
||||
{tournament.canFinalize(user) ? (
|
||||
<div className="tournament-bracket__finalize">
|
||||
<div className={styles.finalize}>
|
||||
<LinkButton
|
||||
variant="minimal"
|
||||
testId="finalize-tournament-button"
|
||||
|
|
@ -193,7 +192,7 @@ export default function TournamentBracketsPage() {
|
|||
<div className="stack sm items-center">
|
||||
<Alert
|
||||
variation="INFO"
|
||||
alertClassName="tournament-bracket__start-bracket-alert"
|
||||
alertClassName={styles.startBracketAlert}
|
||||
textClassName="stack horizontal md items-center"
|
||||
>
|
||||
{bracket.participantTournamentTeamIds.length}/
|
||||
|
|
@ -203,7 +202,7 @@ export default function TournamentBracketsPage() {
|
|||
) : null}
|
||||
</Alert>
|
||||
{!bracket.canBeStarted ? (
|
||||
<div className="tournament-bracket__mini-alert">
|
||||
<div className={styles.miniAlert}>
|
||||
⚠️{" "}
|
||||
{bracket.isStartingBracket
|
||||
? "Tournament start time is in the future"
|
||||
|
|
@ -461,16 +460,11 @@ function BracketNav({
|
|||
{/** MOBILE */}
|
||||
<SendouMenu
|
||||
trigger={
|
||||
<SendouButton
|
||||
className={clsx(
|
||||
"tournament-bracket__bracket-nav__link",
|
||||
"tournament-bracket__menu",
|
||||
)}
|
||||
>
|
||||
<SendouButton className={clsx(styles.bracketNavLink, styles.menu)}>
|
||||
{bracketNameForButton(
|
||||
tournament.bracketByIdxOrDefault(bracketIdx).name,
|
||||
)}
|
||||
<span className="tournament-bracket__bracket-nav__chevron">▼</span>
|
||||
<span className={styles.bracketNavChevron}>▼</span>
|
||||
</SendouButton>
|
||||
}
|
||||
>
|
||||
|
|
@ -485,15 +479,14 @@ function BracketNav({
|
|||
))}
|
||||
</SendouMenu>
|
||||
{/** DESKTOP */}
|
||||
<div className="tournament-bracket__bracket-nav tournament-bracket__button-row">
|
||||
<div className={clsx(styles.bracketNav, styles.buttonRow)}>
|
||||
{visibleBrackets.map((bracket, i) => {
|
||||
return (
|
||||
<SendouButton
|
||||
key={bracket.name}
|
||||
onPress={() => setBracketIdx(i)}
|
||||
className={clsx("tournament-bracket__bracket-nav__link", {
|
||||
"tournament-bracket__bracket-nav__link__selected":
|
||||
bracketIdx === i,
|
||||
className={clsx(styles.bracketNavLink, {
|
||||
[styles.bracketNavLinkSelected]: bracketIdx === i,
|
||||
})}
|
||||
>
|
||||
{bracketNameForButton(bracket.name)}
|
||||
|
|
@ -513,7 +506,7 @@ function CompactifyButton() {
|
|||
onPress={() => {
|
||||
setBracketExpanded(!bracketExpanded);
|
||||
}}
|
||||
className="tournament-bracket__compactify-button"
|
||||
className={styles.compactifyButton}
|
||||
icon={bracketExpanded ? <EyeSlashIcon /> : <EyeIcon />}
|
||||
>
|
||||
{bracketExpanded ? "Compactify" : "Show all"}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ import {
|
|||
} from "../tournament-bracket-utils";
|
||||
export { action, loader };
|
||||
|
||||
import "../tournament-bracket.css";
|
||||
import tournamentStyles from "../../tournament/tournament.module.css";
|
||||
import styles from "../tournament-bracket.module.css";
|
||||
|
||||
export default function TournamentMatchPage() {
|
||||
const user = useUser();
|
||||
|
|
@ -172,12 +173,12 @@ function BeforeMatchChat() {
|
|||
}, [data.match.chatCode]);
|
||||
|
||||
return (
|
||||
<div className="tournament__action-section mt-6">
|
||||
<div className={clsx(tournamentStyles.actionSection, "mt-6")}>
|
||||
<ConnectedChat
|
||||
rooms={rooms}
|
||||
users={chatUsers}
|
||||
className="tournament__chat-container"
|
||||
messagesContainerClassName="tournament__chat-messages-container"
|
||||
className={tournamentStyles.chatContainer}
|
||||
messagesContainerClassName={tournamentStyles.chatMessagesContainer}
|
||||
missingUserName="???"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -386,8 +387,8 @@ function EndedEarlyMessage() {
|
|||
const winnerTeam = winnerTeamId ? tournament.teamById(winnerTeamId) : null;
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__during-match-actions">
|
||||
<div className="tournament-bracket__locked-banner tournament-bracket__locked-banner__lonely">
|
||||
<div className={styles.duringMatchActions}>
|
||||
<div className={clsx(styles.lockedBanner, styles.lockedBannerLonely)}>
|
||||
<div className="stack sm items-center">
|
||||
<div className="text-lg text-center font-bold">Match ended early</div>
|
||||
{winnerTeam ? (
|
||||
|
|
@ -402,7 +403,7 @@ function EndedEarlyMessage() {
|
|||
<Form method="post" className="contents">
|
||||
<SubmitButton
|
||||
_action="REOPEN_MATCH"
|
||||
className="tournament-bracket__stage-banner__undo-button"
|
||||
className={styles.stageBannerUndoButton}
|
||||
testId="reopen-match-button"
|
||||
>
|
||||
Reopen match
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.tournament-bracket__finalize {
|
||||
.finalize {
|
||||
font-size: var(--fonts-sm);
|
||||
margin-block-end: var(--s-4);
|
||||
display: flex;
|
||||
|
|
@ -7,18 +7,18 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__start-bracket-alert {
|
||||
.startBracketAlert {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tournament-bracket__mini-alert {
|
||||
.miniAlert {
|
||||
background-color: var(--color-info-low);
|
||||
font-size: var(--fonts-xxs);
|
||||
border-radius: var(--rounded);
|
||||
padding: var(--s-1) var(--s-2);
|
||||
}
|
||||
|
||||
.tournament-bracket__infos {
|
||||
.infos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -33,15 +33,15 @@
|
|||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.tournament-bracket__infos__label {
|
||||
.infosLabel {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.tournament-bracket__infos__value > button {
|
||||
.infosValue > button {
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.tournament-bracket__locked-banner {
|
||||
.lockedBanner {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
background-color: var(--color-bg-higher);
|
||||
|
|
@ -54,11 +54,11 @@
|
|||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament-bracket__locked-banner__lonely {
|
||||
.lockedBannerLonely {
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner {
|
||||
.stageBanner {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__top-bar {
|
||||
.stageBannerTopBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-2);
|
||||
|
|
@ -86,13 +86,13 @@
|
|||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__bottom-bar {
|
||||
.stageBannerBottomBar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__undo-button {
|
||||
.stageBannerUndoButton {
|
||||
border: 0;
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-text);
|
||||
|
|
@ -104,21 +104,17 @@
|
|||
bottom: 8px;
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__end-set-button {
|
||||
.stageBannerEndSetButton {
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner:has(
|
||||
.tournament-bracket__stage-banner__end-set-button
|
||||
)
|
||||
.tournament-bracket__stage-banner__undo-button:not(
|
||||
.tournament-bracket__stage-banner__end-set-button
|
||||
) {
|
||||
.stageBanner:has(.stageBannerEndSetButton)
|
||||
.stageBannerUndoButton:not(.stageBannerEndSetButton) {
|
||||
right: 72px;
|
||||
}
|
||||
|
||||
.tournament-bracket__deadline-popover {
|
||||
.deadlinePopover {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
|
|
@ -127,12 +123,12 @@
|
|||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.tournament-bracket__deadline-popover__trigger {
|
||||
.deadlinePopoverTrigger {
|
||||
margin-block-start: 0;
|
||||
background-color: var(--color-bg-higher) !important;
|
||||
}
|
||||
|
||||
.tournament-bracket__deadline-indicator {
|
||||
.deadlineIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -143,56 +139,56 @@
|
|||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.tournament-bracket__deadline-indicator__warning {
|
||||
.deadlineIndicatorWarning {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.tournament-bracket__deadline-indicator__error {
|
||||
.deadlineIndicatorError {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__top-bar__header {
|
||||
.stageBannerTopBarHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__top-bar__map-text-big {
|
||||
.stageBannerTopBarMapTextBig {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.tournament-bracket__stage-banner__top-bar {
|
||||
.stageBannerTopBar {
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__top-bar__map-text-small {
|
||||
.stageBannerTopBarMapTextSmall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tournament-bracket__stage-banner__top-bar__map-text-big {
|
||||
.stageBannerTopBarMapTextBig {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament-bracket__no-screen {
|
||||
.noScreen {
|
||||
display: flex;
|
||||
gap: var(--s-0-5);
|
||||
}
|
||||
|
||||
.tournament-bracket__no-screen > svg {
|
||||
.noScreen > svg {
|
||||
width: 24px;
|
||||
fill: var(--color-success);
|
||||
}
|
||||
|
||||
.tournament-bracket__no-screen__banned > svg {
|
||||
.noScreenBanned > svg {
|
||||
fill: var(--color-error);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions {
|
||||
.duringMatchActions {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"img"
|
||||
|
|
@ -201,27 +197,27 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__actions {
|
||||
.duringMatchActionsActions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--color-warning);
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__amount-warning-paragraph {
|
||||
.duringMatchActionsAmountWarningParagraph {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__confirm-score-text {
|
||||
.duringMatchActionsConfirmScoreText {
|
||||
font-size: var(--fonts-xs);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__rosters {
|
||||
.duringMatchActionsRosters {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -230,13 +226,13 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__radio-container {
|
||||
.duringMatchActionsRadioContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__roster-buttons__container {
|
||||
.rosterButtonsContainer {
|
||||
display: flex;
|
||||
gap: var(--s-4);
|
||||
align-items: center;
|
||||
|
|
@ -245,11 +241,11 @@
|
|||
height: 30px;
|
||||
}
|
||||
|
||||
.tournament-bracket__edit-roster-button {
|
||||
.editRosterButton {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__team-players {
|
||||
.duringMatchActionsTeamPlayers {
|
||||
display: flex;
|
||||
width: 15rem;
|
||||
flex-direction: column;
|
||||
|
|
@ -258,22 +254,22 @@
|
|||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox-name {
|
||||
.duringMatchActionsCheckboxName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox-name:not(
|
||||
.disabled-opaque
|
||||
):not(.presentational):hover {
|
||||
.duringMatchActionsCheckboxName:not(.disabled-opaque):not(
|
||||
.presentational
|
||||
):hover {
|
||||
border-radius: var(--rounded);
|
||||
cursor: pointer;
|
||||
outline: 2px solid var(--color-accent-low);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox {
|
||||
.duringMatchActionsCheckbox {
|
||||
width: 40% !important;
|
||||
height: 1.5rem !important;
|
||||
appearance: none;
|
||||
|
|
@ -283,11 +279,11 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox:checked {
|
||||
.duringMatchActionsCheckbox:checked {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox::after {
|
||||
.duringMatchActionsCheckbox::after {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -301,12 +297,12 @@
|
|||
padding-block-end: 1px;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__checkbox:checked::after {
|
||||
.duringMatchActionsCheckbox:checked::after {
|
||||
color: var(--color-text-inverse);
|
||||
content: "Playing";
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__player-name {
|
||||
.duringMatchActionsPlayerName {
|
||||
display: flex;
|
||||
width: 60%;
|
||||
height: 1.5rem;
|
||||
|
|
@ -323,7 +319,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__player-name__inner {
|
||||
.duringMatchActionsPlayerNameInner {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -331,17 +327,17 @@
|
|||
max-width: 150px;
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__seed {
|
||||
.duringMatchActionsSeed {
|
||||
font-size: var(--fonts-xxs);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.tournament-bracket__during-match-actions__team-name {
|
||||
.duringMatchActionsTeamName {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.tournament-bracket__rosters {
|
||||
.rosters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-8);
|
||||
|
|
@ -350,25 +346,25 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tournament-bracket__rosters ul {
|
||||
.rosters ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tournament-bracket__rosters__spaced-header {
|
||||
.rostersSpacedHeader {
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.tournament-bracket__rosters {
|
||||
.rosters {
|
||||
justify-content: space-evenly;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress {
|
||||
.modeProgress {
|
||||
display: block;
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
|
|
@ -376,13 +372,13 @@
|
|||
margin-bottom: var(--s-1-5);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__inner {
|
||||
.modeProgressInner {
|
||||
display: inline-flex;
|
||||
gap: var(--s-4);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image {
|
||||
.modeProgressImage {
|
||||
background-color: var(--color-bg-high);
|
||||
border-radius: 100%;
|
||||
padding: var(--s-2-5);
|
||||
|
|
@ -390,63 +386,63 @@
|
|||
min-width: 40px;
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image > svg {
|
||||
.modeProgressImage > svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__notable {
|
||||
.modeProgressImageNotable {
|
||||
background-color: var(--color-bg-higher);
|
||||
border: none;
|
||||
outline: 2px solid var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__team-one-win {
|
||||
.modeProgressImageTeamOneWin {
|
||||
outline: 2px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__team-two-win {
|
||||
.modeProgressImageTeamTwoWin {
|
||||
outline: 2px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__selected {
|
||||
.modeProgressImageSelected {
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__banned {
|
||||
.modeProgressImageBanned {
|
||||
outline: 2px solid var(--color-error);
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__banned > img {
|
||||
.modeProgressImageBanned > img {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.tournament-bracket__mode-progress__image__banned__popover-trigger:focus-visible {
|
||||
.modeProgressImageBannedPopoverTrigger:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.tournament-bracket__team-one-dot {
|
||||
.teamOneDot {
|
||||
border-radius: 100%;
|
||||
background-color: var(--color-accent);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.tournament-bracket__team-two-dot {
|
||||
.teamTwoDot {
|
||||
border-radius: 100%;
|
||||
background-color: var(--color-accent);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.tournament-bracket__points-input {
|
||||
.pointsInput {
|
||||
--input-width: 4.5rem;
|
||||
padding: var(--s-3-5) var(--s-2) !important;
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.tournament-bracket__progress {
|
||||
.progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-higher);
|
||||
|
|
@ -457,17 +453,17 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.tournament-bracket__infos {
|
||||
.infos {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav {
|
||||
.bracketNav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link {
|
||||
.bracketNavLink {
|
||||
font-size: var(--fonts-xxs);
|
||||
color: var(--color-text-high);
|
||||
border-color: var(--color-bg-higher);
|
||||
|
|
@ -475,36 +471,34 @@
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link__big {
|
||||
.bracketNavLinkBig {
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link:active {
|
||||
.bracketNavLink:active {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link:first-of-type {
|
||||
.bracketNavLink:first-of-type {
|
||||
border-start-start-radius: var(--rounded);
|
||||
border-end-start-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link:not(
|
||||
.tournament-bracket__bracket-nav__link:first-of-type
|
||||
) {
|
||||
.bracketNavLink:not(.bracketNavLink:first-of-type) {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link:last-of-type {
|
||||
.bracketNavLink:last-of-type {
|
||||
border-start-end-radius: var(--rounded);
|
||||
border-end-end-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__link__selected {
|
||||
.bracketNavLinkSelected {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.tournament-bracket__quick-action {
|
||||
.quickAction {
|
||||
font-size: var(--fonts-xs);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
|
|
@ -516,42 +510,42 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__quick-action__spaced {
|
||||
.quickActionSpaced {
|
||||
display: flex;
|
||||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.tournament-bracket__quick-action__very-spaced {
|
||||
.quickActionVerySpaced {
|
||||
display: flex;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.tournament-bracket__quick-action__checkmark {
|
||||
.quickActionCheckmark {
|
||||
width: 1rem;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.tournament-bracket__bracket-nav__chevron {
|
||||
.bracketNavChevron {
|
||||
margin-inline-start: var(--s-2);
|
||||
font-size: var(--fonts-xxxs);
|
||||
margin-block-end: -2px;
|
||||
}
|
||||
|
||||
.tournament-bracket__button-row {
|
||||
.buttonRow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
.tournament-bracket__menu {
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tournament-bracket__button-row {
|
||||
.buttonRow {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament-bracket__compactify-button {
|
||||
.compactifyButton {
|
||||
font-size: var(--fonts-xxs);
|
||||
color: var(--color-text-high);
|
||||
border-color: var(--color-bg-higher);
|
||||
|
|
@ -559,12 +553,12 @@
|
|||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament-bracket__compactify-button svg {
|
||||
.compactifyButton svg {
|
||||
min-width: 0.85rem;
|
||||
max-width: 0.85rem;
|
||||
}
|
||||
|
||||
.tournament-bracket__cast-info-container {
|
||||
.castInfoContainer {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -572,7 +566,7 @@
|
|||
width: max-content;
|
||||
}
|
||||
|
||||
.tournament-bracket__cast-info-container__label {
|
||||
.castInfoContainerLabel {
|
||||
padding: var(--s-2) var(--s-3-5);
|
||||
text-transform: uppercase;
|
||||
background-color: var(--color-bg-higher);
|
||||
|
|
@ -584,17 +578,17 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__cast-info-container__content {
|
||||
.castInfoContainerContent {
|
||||
padding-block: var(--s-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament-bracket__stream-popover {
|
||||
.streamPopover {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.finalize__badge-container {
|
||||
.finalizeBadgeContainer {
|
||||
padding: var(--s-2);
|
||||
background-color: black;
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -7,6 +7,7 @@ import { useIsMounted } from "~/hooks/useIsMounted";
|
|||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { databaseTimestampToDate, nullPaddedDatesOfMonth } from "~/utils/dates";
|
||||
import type { loader } from "../loaders/org.$slug.server";
|
||||
import styles from "../tournament-organization.module.css";
|
||||
|
||||
interface EventCalendarProps {
|
||||
month: number;
|
||||
|
|
@ -33,11 +34,11 @@ export function EventCalendar({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="org__calendar__container">
|
||||
<div className={styles.calendarContainer}>
|
||||
<MonthSelector month={month} year={year} />
|
||||
<div className="org__calendar">
|
||||
<div className={styles.calendar}>
|
||||
{dayHeaders.map((day) => (
|
||||
<div key={day} className="org__calendar__day-header">
|
||||
<div key={day} className={styles.calendarDayHeader}>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -79,19 +80,19 @@ function EventCalendarCell({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx("org__calendar__day", {
|
||||
org__calendar__day__previous: !date,
|
||||
org__calendar__day__today:
|
||||
className={clsx(styles.calendarDay, {
|
||||
[styles.calendarDayPrevious]: !date,
|
||||
[styles.calendarDayToday]:
|
||||
isMounted &&
|
||||
date?.getDate() === new Date().getDate() &&
|
||||
date?.getMonth() === new Date().getMonth() &&
|
||||
date?.getFullYear() === new Date().getFullYear(),
|
||||
})}
|
||||
>
|
||||
<div className="org__calendar__day__date">{date?.getUTCDate()}</div>
|
||||
<div className={styles.calendarDayDate}>{date?.getUTCDate()}</div>
|
||||
{events.length === 1 ? (
|
||||
<img
|
||||
className="org__calendar__day__logo"
|
||||
className={styles.calendarDayLogo}
|
||||
src={events[0].logoUrl ?? fallbackLogoUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
|
|
@ -99,7 +100,7 @@ function EventCalendarCell({
|
|||
/>
|
||||
) : null}
|
||||
{events.length > 1 ? (
|
||||
<div className="org__calendar__day__many-events">{events.length}</div>
|
||||
<div className={styles.calendarDayManyEvents}>{events.length}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -115,7 +116,7 @@ function MonthSelector({ month, year }: { month: number; year: number }) {
|
|||
const { formatDate } = useTimeFormat();
|
||||
|
||||
return (
|
||||
<div className="org__calendar__month-selector">
|
||||
<div className={styles.calendarMonthSelector}>
|
||||
<LinkButton
|
||||
variant="minimal"
|
||||
aria-label="Previous month"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { BskyIcon } from "~/components/icons/Bsky";
|
|||
import { LinkIcon } from "~/components/icons/Link";
|
||||
import { TwitchIcon } from "~/components/icons/Twitch";
|
||||
import { YouTubeIcon } from "~/components/icons/YouTube";
|
||||
import styles from "../tournament-organization.module.css";
|
||||
|
||||
export function SocialLinksList({ links }: { links: string[] }) {
|
||||
return (
|
||||
|
|
@ -18,12 +19,17 @@ function SocialLink({ url }: { url: string }) {
|
|||
const type = urlToLinkType(url);
|
||||
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noreferrer" className="org__social-link">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.socialLink}
|
||||
>
|
||||
<div
|
||||
className={clsx("org__social-link__icon-container", {
|
||||
youtube: type === "youtube",
|
||||
twitch: type === "twitch",
|
||||
bsky: type === "bsky",
|
||||
className={clsx(styles.socialLinkIconContainer, {
|
||||
[styles.socialLinkYoutube]: type === "youtube",
|
||||
[styles.socialLinkTwitch]: type === "twitch",
|
||||
[styles.socialLinkBsky]: type === "bsky",
|
||||
})}
|
||||
>
|
||||
<SocialLinkIcon url={url} />
|
||||
|
|
|
|||
|
|
@ -44,11 +44,10 @@ import { action } from "../actions/org.$slug.server";
|
|||
import { EventCalendar } from "../components/EventCalendar";
|
||||
import { SocialLinksList } from "../components/SocialLinksList";
|
||||
import { loader } from "../loaders/org.$slug.server";
|
||||
import styles from "../tournament-organization.module.css";
|
||||
import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "../tournament-organization-constants";
|
||||
export { action, loader };
|
||||
|
||||
import "../tournament-organization.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
|
|
@ -262,7 +261,7 @@ function AllTournamentsView() {
|
|||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="org__events-container">
|
||||
<div className={styles.eventsContainer}>
|
||||
<EventCalendar
|
||||
month={data.month}
|
||||
year={data.year}
|
||||
|
|
@ -439,7 +438,7 @@ function EventsList({
|
|||
}
|
||||
|
||||
function SectionDivider({ children }: { children: React.ReactNode }) {
|
||||
return <div className="org__section-divider">{children}</div>;
|
||||
return <div className={styles.sectionDivider}>{children}</div>;
|
||||
}
|
||||
|
||||
function EventInfo({
|
||||
|
|
@ -459,14 +458,14 @@ function EventInfo({
|
|||
? tournamentPage(event.tournamentId)
|
||||
: calendarEventPage(event.eventId)
|
||||
}
|
||||
className="org__event-info"
|
||||
className={styles.eventInfo}
|
||||
>
|
||||
{event.logoUrl ? (
|
||||
<img src={event.logoUrl} alt={event.name} width={38} height={38} />
|
||||
) : null}
|
||||
<div>
|
||||
<div className="org__event-info__name">{event.name}</div>
|
||||
<time className="org__event-info__time" suppressHydrationWarning>
|
||||
<div>{event.name}</div>
|
||||
<time className={styles.eventInfoTime} suppressHydrationWarning>
|
||||
{formatDateTime(databaseTimestampToDate(event.startTime), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
|
|
@ -562,7 +561,7 @@ function EventLeaderboard({
|
|||
<div className="stack md">
|
||||
{ownEntry ? (
|
||||
<>
|
||||
<ol className="org__leaderboard-list" start={ownEntry.placement}>
|
||||
<ol className={styles.leaderboardList} start={ownEntry.placement}>
|
||||
<li>
|
||||
<EventLeaderboardRow entry={ownEntry.entry} />
|
||||
</li>
|
||||
|
|
@ -570,7 +569,7 @@ function EventLeaderboard({
|
|||
<Divider />
|
||||
</>
|
||||
) : null}
|
||||
<ol className="org__leaderboard-list">
|
||||
<ol className={styles.leaderboardList}>
|
||||
{leaderboard.map((entry) => (
|
||||
<li key={entry.user.discordId}>
|
||||
<EventLeaderboardRow entry={entry} />
|
||||
|
|
@ -589,7 +588,7 @@ function EventLeaderboardRow({
|
|||
>[number];
|
||||
}) {
|
||||
return (
|
||||
<div className="org__leaderboard-list__row">
|
||||
<div className={styles.leaderboardListRow}>
|
||||
<Link
|
||||
to={userPage(entry.user)}
|
||||
className="stack horizontal sm items-center font-semi-bold text-main-forced"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.org__calendar {
|
||||
.calendar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, max-content);
|
||||
gap: var(--s-2);
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.org__calendar__container {
|
||||
.calendarContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -14,12 +14,12 @@
|
|||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.org__calendar__day-header {
|
||||
.calendarDayHeader {
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.org__calendar__day {
|
||||
.calendarDay {
|
||||
width: var(--cell-size);
|
||||
height: var(--cell-size);
|
||||
font-size: var(--fonts-xs);
|
||||
|
|
@ -31,20 +31,20 @@
|
|||
place-items: center;
|
||||
}
|
||||
|
||||
.org__calendar__day__date {
|
||||
.calendarDayDate {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.org__calendar__day__logo {
|
||||
.calendarDayLogo {
|
||||
position: absolute;
|
||||
border-radius: var(--rounded);
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
.org__calendar__day__many-events {
|
||||
.calendarDayManyEvents {
|
||||
position: absolute;
|
||||
border-radius: var(--rounded);
|
||||
top: 18px;
|
||||
|
|
@ -58,16 +58,16 @@
|
|||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.org__calendar__day__today {
|
||||
.calendarDayToday {
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.org__calendar__day__previous {
|
||||
.calendarDayPrevious {
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.org__calendar__month-selector {
|
||||
.calendarMonthSelector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -75,16 +75,16 @@
|
|||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.org__calendar__month-selector a {
|
||||
.calendarMonthSelector a {
|
||||
font-size: var(--fonts-xl);
|
||||
}
|
||||
|
||||
.org__calendar__month-selector > div {
|
||||
.calendarMonthSelector > div {
|
||||
font-size: var(--fonts-lg);
|
||||
margin-block-start: var(--s-1);
|
||||
}
|
||||
|
||||
.org__section-divider {
|
||||
.sectionDivider {
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--bold);
|
||||
color: var(--color-text-high);
|
||||
|
|
@ -93,26 +93,26 @@
|
|||
padding-block: var(--s-1);
|
||||
}
|
||||
|
||||
.org__events-container {
|
||||
.eventsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-16);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.org__events-container {
|
||||
.eventsContainer {
|
||||
flex-direction: row;
|
||||
gap: var(--s-8);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
.org__calendar__container {
|
||||
.calendarContainer {
|
||||
position: sticky;
|
||||
top: 47px;
|
||||
}
|
||||
}
|
||||
|
||||
.org__event-info {
|
||||
.eventInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -121,37 +121,37 @@
|
|||
width: max-content;
|
||||
}
|
||||
|
||||
.org__event-info img {
|
||||
.eventInfo img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.org__event-info__time {
|
||||
.eventInfoTime {
|
||||
display: block;
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.org__leaderboard-list {
|
||||
.leaderboardList {
|
||||
display: flex;
|
||||
gap: var(--s-4);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.org__leaderboard-list li::marker {
|
||||
.leaderboardList li::marker {
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--bold);
|
||||
color: var(--color-accent);
|
||||
padding-inline-end: var(--s-2);
|
||||
}
|
||||
|
||||
.org__leaderboard-list__row {
|
||||
.leaderboardListRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
padding-inline-start: var(--s-2-5);
|
||||
}
|
||||
|
||||
.org__social-link {
|
||||
.socialLink {
|
||||
font-size: var(--fonts-sm);
|
||||
color: var(--text-main);
|
||||
display: flex;
|
||||
|
|
@ -160,11 +160,11 @@
|
|||
max-width: max-content;
|
||||
}
|
||||
|
||||
.org__social-link svg {
|
||||
.socialLink svg {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.org__social-link__icon-container {
|
||||
.socialLinkIconContainer {
|
||||
background-color: var(--color-bg-higher);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
|
@ -172,14 +172,14 @@
|
|||
padding: var(--s-2);
|
||||
}
|
||||
|
||||
.org__social-link__icon-container.twitch svg {
|
||||
.socialLinkTwitch svg {
|
||||
fill: #9146ff;
|
||||
}
|
||||
|
||||
.org__social-link__icon-container.youtube svg {
|
||||
.socialLinkYoutube svg {
|
||||
fill: #f00;
|
||||
}
|
||||
|
||||
.org__social-link__icon-container.bsky path {
|
||||
.socialLinkBsky path {
|
||||
fill: #1285fe;
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import { TOURNAMENT_SUB } from "../tournament-subs-constants";
|
|||
export { action, loader };
|
||||
|
||||
import clsx from "clsx";
|
||||
import { mainStyles } from "~/components/Main";
|
||||
import styles from "./to.$id.subs.new.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -38,7 +39,7 @@ export default function NewTournamentSubPage() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="half-width">
|
||||
<div className={mainStyles.narrow}>
|
||||
<Form method="post" className="stack md items-start">
|
||||
<div className="stack">
|
||||
<h2>{t("tournament:subs.addPost")}</h2>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { databaseTimestampToDate } from "~/utils/dates";
|
|||
import { userPage } from "~/utils/urls";
|
||||
import { accountCreatedInTheLastSixMonths } from "~/utils/users";
|
||||
import { useTournament, useTournamentFriendCodes } from "../routes/to.$id";
|
||||
import styles from "../tournament.module.css";
|
||||
|
||||
export function TeamWithRoster({
|
||||
team,
|
||||
|
|
@ -31,29 +32,27 @@ export function TeamWithRoster({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="tournament__team-with-roster">
|
||||
<div className="tournament__team-with-roster__name">
|
||||
<div className={styles.teamWithRoster}>
|
||||
<div className={styles.teamWithRosterName}>
|
||||
<div className="stack horizontal sm justify-end items-end">
|
||||
{teamLogoSrc ? <Avatar size="xxs" url={teamLogoSrc} /> : null}
|
||||
{seed ? (
|
||||
<div className="tournament__team-with-roster__seed">#{seed}</div>
|
||||
<div className={styles.teamWithRosterSeed}>#{seed}</div>
|
||||
) : null}
|
||||
</div>{" "}
|
||||
{teamPageUrl ? (
|
||||
<Link
|
||||
to={teamPageUrl}
|
||||
className="tournament__team-with-roster__team-name"
|
||||
className={styles.teamWithRosterTeamName}
|
||||
data-testid="team-name"
|
||||
>
|
||||
{team.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="tournament__team-with-roster__team-name">
|
||||
{team.name}
|
||||
</span>
|
||||
<span className={styles.teamWithRosterTeamName}>{team.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<ul className="tournament__team-with-roster__members">
|
||||
<ul className={styles.teamWithRosterMembers}>
|
||||
{team.members.map((member) => {
|
||||
const friendCode = friendCodes?.[member.userId];
|
||||
const isSub =
|
||||
|
|
@ -69,20 +68,22 @@ export function TeamWithRoster({
|
|||
};
|
||||
|
||||
return (
|
||||
<li key={member.userId} className="tournament__team-member-row">
|
||||
<li key={member.userId} className={styles.teamMemberRow}>
|
||||
{member.isOwner ? (
|
||||
<span className="tournament__team-member-name__role text-theme">
|
||||
<span className={`${styles.teamMemberNameRole} text-theme`}>
|
||||
C
|
||||
</span>
|
||||
) : null}
|
||||
{isSub && !member.isOwner ? (
|
||||
<span className="tournament__team-member-name__role tournament__team-member-name__role__sub">
|
||||
<span
|
||||
className={`${styles.teamMemberNameRole} ${styles.teamMemberNameRoleSub}`}
|
||||
>
|
||||
S
|
||||
</span>
|
||||
) : null}
|
||||
<div
|
||||
className={clsx("tournament__team-with-roster__member", {
|
||||
"tournament__team-with-roster__member__inactive":
|
||||
className={clsx(styles.teamWithRosterMember, {
|
||||
[styles.teamWithRosterMemberInactive]:
|
||||
activePlayers && !activePlayers.includes(member.userId),
|
||||
})}
|
||||
>
|
||||
|
|
@ -90,13 +91,13 @@ export function TeamWithRoster({
|
|||
user={member}
|
||||
size="xxs"
|
||||
className={clsx({
|
||||
"tournament__team-with-roster__member__avatar-inactive":
|
||||
[styles.teamWithRosterMemberAvatarInactive]:
|
||||
activePlayers && !activePlayers.includes(member.userId),
|
||||
})}
|
||||
/>
|
||||
<Link
|
||||
to={userPage(member)}
|
||||
className="tournament__team-member-name"
|
||||
className={styles.teamMemberName}
|
||||
data-testid="team-member-name"
|
||||
>
|
||||
{name()}
|
||||
|
|
@ -140,16 +141,15 @@ function TeamMapPool({
|
|||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx("tournament__team-with-roster__map-pool", {
|
||||
"tournament__team-with-roster__map-pool__3-columns":
|
||||
mapPool.length % 3 === 0,
|
||||
className={clsx(styles.teamWithRosterMapPool, {
|
||||
[styles.teamWithRosterMapPool3Columns]: mapPool.length % 3 === 0,
|
||||
})}
|
||||
>
|
||||
{mapPool.map(({ mode, stageId }, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<StageImage stageId={stageId} width={85} />
|
||||
<div className="tournament__team-with-roster__map-pool__mode-info">
|
||||
<div className={styles.teamWithRosterMapPoolModeInfo}>
|
||||
<ModeImage mode={mode} size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { twitchThumbnailUrlToSrc } from "~/modules/twitch/utils";
|
|||
import { twitchUrl } from "~/utils/urls";
|
||||
import type { TournamentStreamsLoader } from "../loaders/to.$id.streams.server";
|
||||
import { useTournament } from "../routes/to.$id";
|
||||
import styles from "../tournament.module.css";
|
||||
|
||||
export function TournamentStream({
|
||||
stream,
|
||||
|
|
@ -37,17 +38,17 @@ export function TournamentStream({
|
|||
) : null}
|
||||
<div className="stack md horizontal justify-between">
|
||||
{user && team ? (
|
||||
<div className="tournament__stream__user-container">
|
||||
<div className={styles.streamUserContainer}>
|
||||
<Avatar size="xxs" user={user} /> {user.username}
|
||||
<span className="text-theme-secondary">{team.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tournament__stream__user-container">
|
||||
<div className={styles.streamUserContainer}>
|
||||
<Avatar size="xxs" url={tournament.ctx.logoUrl} />
|
||||
Cast <span className="text-lighter">{stream.twitchUserName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="tournament__stream__viewer-count">
|
||||
<div className={styles.streamViewerCount}>
|
||||
<UserIcon />
|
||||
{stream.viewerCount}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { assertUnreachable } from "~/utils/types";
|
|||
import { userEditProfilePage } from "~/utils/urls";
|
||||
import { action } from "../actions/to.$id.join.server";
|
||||
import { loader } from "../loaders/to.$id.join.server";
|
||||
import styles from "../tournament.module.css";
|
||||
import { validateCanJoinTeam } from "../tournament-utils";
|
||||
import { useTournament } from "./to.$id";
|
||||
export { action, loader };
|
||||
|
|
@ -80,7 +81,7 @@ export default function JoinTeamPage() {
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Form method="post" className="tournament__invite-container">
|
||||
<Form method="post" className={styles.inviteContainer}>
|
||||
{validationStatus === "VALID" ? (
|
||||
<div className="stack md items-center">
|
||||
<SubmitButton size="big" isDisabled={!user?.friendCode}>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import { AlertIcon } from "../../../components/icons/Alert";
|
|||
import { action } from "../actions/to.$id.register.server";
|
||||
import type { TournamentRegisterPageLoader } from "../loaders/to.$id.register.server";
|
||||
import { loader } from "../loaders/to.$id.register.server";
|
||||
import styles from "../tournament.module.css";
|
||||
import { TOURNAMENT } from "../tournament-constants";
|
||||
import {
|
||||
type CounterPickValidationStatus,
|
||||
|
|
@ -76,16 +77,16 @@ export default function TournamentRegisterPage() {
|
|||
|
||||
return (
|
||||
<div className={clsx("stack lg", containerClassName("normal"))}>
|
||||
<div className="tournament__logo-container">
|
||||
<div className={styles.logoContainer}>
|
||||
<img
|
||||
src={tournament.ctx.logoUrl}
|
||||
alt=""
|
||||
className="tournament__logo"
|
||||
className={styles.logo}
|
||||
width={124}
|
||||
height={124}
|
||||
/>
|
||||
<div>
|
||||
<div className="tournament__title">{tournament.ctx.name}</div>
|
||||
<div className={styles.title}>{tournament.ctx.name}</div>
|
||||
<div>
|
||||
{tournament.ctx.organization ? (
|
||||
<Link
|
||||
|
|
@ -106,15 +107,15 @@ export default function TournamentRegisterPage() {
|
|||
to={userPage(tournament.ctx.author)}
|
||||
className="stack horizontal xs items-center text-lighter"
|
||||
>
|
||||
<UserIcon className="tournament__info__icon" />{" "}
|
||||
<UserIcon className={styles.infoIcon} />{" "}
|
||||
{tournament.ctx.author.username}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{!tournament.isLeagueSignup ? (
|
||||
<div className="tournament__by mt-2">
|
||||
<div className={clsx(styles.by, "mt-2")}>
|
||||
<div className="stack horizontal xs items-center">
|
||||
<ClockIcon className="tournament__info__icon" />{" "}
|
||||
<ClockIcon className={styles.infoIcon} />{" "}
|
||||
{isMounted ? (
|
||||
<TimePopover
|
||||
time={tournament.ctx.startTime}
|
||||
|
|
@ -131,15 +132,15 @@ export default function TournamentRegisterPage() {
|
|||
) : null}
|
||||
<div className="stack horizontal sm mt-1">
|
||||
{tournament.ranked ? (
|
||||
<div className="tournament__badge tournament__badge__ranked">
|
||||
<div className={clsx(styles.badge, styles.badgeRanked)}>
|
||||
Ranked
|
||||
</div>
|
||||
) : (
|
||||
<div className="tournament__badge tournament__badge__unranked">
|
||||
<div className={clsx(styles.badge, styles.badgeUnranked)}>
|
||||
Unranked
|
||||
</div>
|
||||
)}
|
||||
<div className="tournament__badge tournament__badge__modes">
|
||||
<div className={clsx(styles.badge, styles.badgeModes)}>
|
||||
{tournament.modesIncluded.map((mode) => (
|
||||
<ModeImage key={mode} mode={mode} size={16} />
|
||||
))}
|
||||
|
|
@ -216,7 +217,7 @@ function TournamentRegisterInfoTabs() {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="tournament__info__description">
|
||||
<div className={styles.infoDescription}>
|
||||
<Markdown options={{ wrapper: React.Fragment }}>
|
||||
{tournament.ctx.description ?? ""}
|
||||
</Markdown>
|
||||
|
|
@ -228,7 +229,7 @@ function TournamentRegisterInfoTabs() {
|
|||
|
||||
{tournament.ctx.rules ? (
|
||||
<SendouTabPanel id="rules">
|
||||
<div className="tournament__info__description">
|
||||
<div className={styles.infoDescription}>
|
||||
<Markdown options={{ wrapper: React.Fragment }}>
|
||||
{tournament.ctx.rules ?? ""}
|
||||
</Markdown>
|
||||
|
|
@ -436,10 +437,10 @@ function RegistrationProgress({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="tournament__section-header text-center">
|
||||
<h3 className={clsx(styles.sectionHeader, "text-center")}>
|
||||
{t("tournament:pre.steps.header")}
|
||||
</h3>
|
||||
<section className="tournament__section stack md">
|
||||
<section className={clsx(styles.section, "stack md")}>
|
||||
<div className="stack horizontal lg justify-center text-sm font-semi-bold">
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
|
|
@ -450,13 +451,17 @@ function RegistrationProgress({
|
|||
{step.name}
|
||||
{step.status === "completed" ? (
|
||||
<CheckmarkIcon
|
||||
className="tournament__section__icon fill-success"
|
||||
className={clsx(styles.sectionIcon, "fill-success")}
|
||||
testId={`checkmark-icon-num-${i + 1}`}
|
||||
/>
|
||||
) : step.status === "notice" ? (
|
||||
<AlertIcon className="tournament__section__icon fill-info p-1" />
|
||||
<AlertIcon
|
||||
className={clsx(styles.sectionIcon, "fill-info p-1")}
|
||||
/>
|
||||
) : (
|
||||
<CrossIcon className="tournament__section__icon fill-error" />
|
||||
<CrossIcon
|
||||
className={clsx(styles.sectionIcon, "fill-error")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -480,7 +485,7 @@ function RegistrationProgress({
|
|||
/>
|
||||
) : null}
|
||||
</section>
|
||||
<div className="tournament__section__warning">
|
||||
<div className={styles.sectionWarning}>
|
||||
{regClosesBeforeStart || tournament.isLeagueSignup ? (
|
||||
<span className="text-warning">
|
||||
Registration closes at {registrationClosesAtString}
|
||||
|
|
@ -663,7 +668,7 @@ function TeamInfo({
|
|||
return (
|
||||
<div>
|
||||
<div className="stack horizontal justify-between">
|
||||
<h3 className="tournament__section-header">
|
||||
<h3 className={styles.sectionHeader}>
|
||||
2. {t("tournament:pre.info.header")}
|
||||
</h3>
|
||||
{canUnregister &&
|
||||
|
|
@ -699,7 +704,7 @@ function TeamInfo({
|
|||
</FormWithConfirm>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="tournament__section">
|
||||
<section className={styles.section}>
|
||||
<Form method="post" className="stack md items-center" ref={ref}>
|
||||
<input type="hidden" name="_action" value="UPSERT_TEAM" />
|
||||
{signUpWithTeamId ? (
|
||||
|
|
@ -707,7 +712,7 @@ function TeamInfo({
|
|||
) : null}
|
||||
<div className="stack sm-plus items-center">
|
||||
{data && data.teams.length > 0 && tournament.registrationOpen ? (
|
||||
<div className="tournament__section__input-container">
|
||||
<div className={styles.sectionInputContainer}>
|
||||
<Label htmlFor="signingUpAs">Team signing up as</Label>
|
||||
<select
|
||||
id="signingUpAs"
|
||||
|
|
@ -733,7 +738,7 @@ function TeamInfo({
|
|||
) : null}
|
||||
|
||||
{!signUpWithTeamId ? (
|
||||
<div className="tournament__section__input-container">
|
||||
<div className={styles.sectionInputContainer}>
|
||||
<Label htmlFor="teamName">
|
||||
{data && data.teams.length > 0
|
||||
? "Pick-up name"
|
||||
|
|
@ -755,7 +760,7 @@ function TeamInfo({
|
|||
<input type="hidden" name="teamName" value={teamName} />
|
||||
)}
|
||||
{tournament.registrationOpen || avatarUrl ? (
|
||||
<div className="tournament__section__input-container">
|
||||
<div className={styles.sectionInputContainer}>
|
||||
<Label htmlFor="logo">Logo</Label>
|
||||
{avatarUrl ? (
|
||||
<div className="stack horizontal md items-center">
|
||||
|
|
@ -863,14 +868,14 @@ function FriendCode() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="tournament__section-header">1. Friend code</h3>
|
||||
<section className="tournament__section">
|
||||
<div className="tournament__section__input-container mx-auto">
|
||||
<h3 className={styles.sectionHeader}>1. Friend code</h3>
|
||||
<section className={styles.section}>
|
||||
<div className={clsx(styles.sectionInputContainer, "mx-auto")}>
|
||||
<FriendCodeInput friendCode={user?.friendCode} />
|
||||
</div>
|
||||
</section>
|
||||
{user?.friendCode ? (
|
||||
<div className="tournament__section__warning">
|
||||
<div className={styles.sectionWarning}>
|
||||
Is the friend code above wrong? Post a message on the{" "}
|
||||
<a
|
||||
href={SENDOU_INK_DISCORD_URL}
|
||||
|
|
@ -889,10 +894,10 @@ function FriendCode() {
|
|||
function GoogleFormsLink() {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="tournament__section-header">
|
||||
<h3 className={styles.sectionHeader}>
|
||||
Additional Requirement: Google Form
|
||||
</h3>
|
||||
<section className="tournament__section stack lg items-center">
|
||||
<section className={clsx(styles.section, "stack lg items-center")}>
|
||||
<a
|
||||
href={import.meta.env.VITE_LEAGUE_GOOGLE_FORM_URL}
|
||||
className="py-4 font-bold"
|
||||
|
|
@ -902,7 +907,7 @@ function GoogleFormsLink() {
|
|||
Answer survey hosted on Google Forms
|
||||
</a>
|
||||
</section>
|
||||
<div className="tournament__section__warning">
|
||||
<div className={styles.sectionWarning}>
|
||||
Answer to additional question about your team's preferred match time and
|
||||
info to help with seeding
|
||||
</div>
|
||||
|
|
@ -963,10 +968,10 @@ function FillRoster({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="tournament__section-header">
|
||||
<h3 className={styles.sectionHeader}>
|
||||
3. {t("tournament:pre.roster.header")}
|
||||
</h3>
|
||||
<section className="tournament__section stack lg items-center">
|
||||
<section className={clsx(styles.section, "stack lg items-center")}>
|
||||
{playersAvailableToDirectlyAdd.length > 0 && canAddMembers ? (
|
||||
<>
|
||||
<DirectlyAddPlayerSelect
|
||||
|
|
@ -992,7 +997,7 @@ function FillRoster({
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="tournament__roster-grid">
|
||||
<div className={styles.rosterGrid}>
|
||||
{ownTeamMembers.map((member, i) => {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -1002,7 +1007,7 @@ function FillRoster({
|
|||
>
|
||||
<Avatar size="xsm" user={member} />
|
||||
{tournament.ctx.settings.requireInGameNames ? (
|
||||
<div className="tournament__roster-grid__member-name">
|
||||
<div className={styles.rosterGridMemberName}>
|
||||
<div className="text-center">
|
||||
{member.inGameName ?? member.username}
|
||||
</div>
|
||||
|
|
@ -1013,7 +1018,7 @@ function FillRoster({
|
|||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tournament__roster-grid__member-name">
|
||||
<div className={styles.rosterGridMemberName}>
|
||||
{member.username}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1022,7 +1027,7 @@ function FillRoster({
|
|||
})}
|
||||
{new Array(missingMembers).fill(null).map((_, i) => {
|
||||
return (
|
||||
<div key={i} className="tournament__missing-player">
|
||||
<div key={i} className={styles.missingPlayer}>
|
||||
?
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1031,7 +1036,10 @@ function FillRoster({
|
|||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="tournament__missing-player tournament__missing-player__optional"
|
||||
className={clsx(
|
||||
styles.missingPlayer,
|
||||
styles.missingPlayerOptional,
|
||||
)}
|
||||
>
|
||||
?
|
||||
</div>
|
||||
|
|
@ -1043,13 +1051,13 @@ function FillRoster({
|
|||
) : null}
|
||||
</section>
|
||||
{tournament.ctx.settings.requireInGameNames ? (
|
||||
<div className="tournament__section__warning text-warning-important">
|
||||
<div className={clsx(styles.sectionWarning, "text-warning-important")}>
|
||||
Note that you are expected to use the in-game names as listed above.
|
||||
Playing in the event with a different name or using the alias feature
|
||||
might result in disqualification.
|
||||
</div>
|
||||
) : (
|
||||
<div className="tournament__section__warning">
|
||||
<div className={styles.sectionWarning}>
|
||||
{tournament.minMembersPerTeam <= 3
|
||||
? t("tournament:pre.roster.footer.noSubs", {
|
||||
format: `${tournament.minMembersPerTeam}v${tournament.minMembersPerTeam}`,
|
||||
|
|
@ -1184,10 +1192,10 @@ function CounterPickMapPoolPicker() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="tournament__section-header">
|
||||
<h3 className={styles.sectionHeader}>
|
||||
4. {t("tournament:pre.pool.header")}
|
||||
</h3>
|
||||
<section className="tournament__section">
|
||||
<section className={styles.section}>
|
||||
<fetcher.Form method="post" className="stack lg">
|
||||
<input
|
||||
type="hidden"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
tournamentTeamPage,
|
||||
} from "~/utils/urls";
|
||||
import * as Standings from "../core/Standings";
|
||||
import styles from "../tournament.module.css";
|
||||
import { useTournament } from "./to.$id";
|
||||
|
||||
export default function TournamentResultsPage() {
|
||||
|
|
@ -135,7 +136,7 @@ function ResultsTable({ standings }: { standings: Standing[] }) {
|
|||
tournamentId: tournament.ctx.id,
|
||||
tournamentTeamId: standing.team.id,
|
||||
})}
|
||||
className="tournament__standings__team-name"
|
||||
className={styles.standingsTeamName}
|
||||
data-testid="result-team-name"
|
||||
>
|
||||
{teamLogoSrc ? <Avatar size="xs" url={teamLogoSrc} /> : null}{" "}
|
||||
|
|
@ -190,7 +191,7 @@ function MatchHistoryRow({ teamId }: { teamId: number }) {
|
|||
return (
|
||||
<React.Fragment key={match.id}>
|
||||
{bracketChanged ? (
|
||||
<div className="tournament__standings__divider" />
|
||||
<div className={styles.standingsDivider} />
|
||||
) : null}
|
||||
<MatchResultSquare result={match.result} matchId={match.id}>
|
||||
{match.vsSeed}
|
||||
|
|
@ -219,9 +220,9 @@ function MatchResultSquare({
|
|||
matchId,
|
||||
tournamentId: tournament.ctx.id,
|
||||
})}
|
||||
className={clsx("tournament__standings__match-result-square", {
|
||||
"tournament__standings__match-result-square--win": result === "win",
|
||||
"tournament__standings__match-result-square--loss": result === "loss",
|
||||
className={clsx(styles.standingsMatchResultSquare, {
|
||||
[styles.standingsMatchResultSquareWin]: result === "win",
|
||||
[styles.standingsMatchResultSquareLoss]: result === "loss",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { InfoPopover } from "../../../components/InfoPopover";
|
|||
import { ordinalToRoundedSp } from "../../mmr/mmr-utils";
|
||||
import { action } from "../actions/to.$id.seeds.server";
|
||||
import { loader } from "../loaders/to.$id.seeds.server";
|
||||
import styles from "../tournament.module.css";
|
||||
import { useTournament } from "./to.$id";
|
||||
export { loader, action };
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ export default function TournamentSeedsPage() {
|
|||
</div>
|
||||
) : (
|
||||
<SendouButton
|
||||
className="tournament__seeds__order-button"
|
||||
className={styles.seedsOrderButton}
|
||||
variant="minimal"
|
||||
size="small"
|
||||
type="button"
|
||||
|
|
@ -118,11 +119,16 @@ export default function TournamentSeedsPage() {
|
|||
/>
|
||||
) : null}
|
||||
<ul>
|
||||
<li className="tournament__seeds__teams-list-row">
|
||||
<div className="tournament__seeds__teams-container__header" />
|
||||
<div className="tournament__seeds__teams-container__header" />
|
||||
<div className="tournament__seeds__teams-container__header">Name</div>
|
||||
<div className="tournament__seeds__teams-container__header stack horizontal xxs">
|
||||
<li className={styles.seedsTeamsListRow}>
|
||||
<div className={styles.seedsTeamsContainerHeader} />
|
||||
<div className={styles.seedsTeamsContainerHeader} />
|
||||
<div className={styles.seedsTeamsContainerHeader}>Name</div>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.seedsTeamsContainerHeader,
|
||||
"stack horizontal xxs",
|
||||
)}
|
||||
>
|
||||
SP
|
||||
<InfoPopover tiny>
|
||||
Seeding point is a value that tracks players' head-to-head
|
||||
|
|
@ -130,9 +136,7 @@ export default function TournamentSeedsPage() {
|
|||
different points.
|
||||
</InfoPopover>
|
||||
</div>
|
||||
<div className="tournament__seeds__teams-container__header">
|
||||
Players
|
||||
</div>
|
||||
<div className={styles.seedsTeamsContainerHeader}>Players</div>
|
||||
</li>
|
||||
<DndContext
|
||||
id="team-seed-sorter"
|
||||
|
|
@ -170,14 +174,10 @@ export default function TournamentSeedsPage() {
|
|||
id={team.id}
|
||||
testId={`seed-team-${team.id}`}
|
||||
disabled={navigation.state !== "idle"}
|
||||
liClassName={clsx(
|
||||
"tournament__seeds__teams-list-row",
|
||||
"sortable",
|
||||
{
|
||||
disabled: navigation.state !== "idle",
|
||||
invisible: activeTeam?.id === team.id,
|
||||
},
|
||||
)}
|
||||
liClassName={clsx(styles.seedsTeamsListRow, "sortable", {
|
||||
disabled: navigation.state !== "idle",
|
||||
invisible: activeTeam?.id === team.id,
|
||||
})}
|
||||
>
|
||||
<RowContents
|
||||
team={team}
|
||||
|
|
@ -195,7 +195,7 @@ export default function TournamentSeedsPage() {
|
|||
|
||||
<DragOverlay>
|
||||
{activeTeam && (
|
||||
<li className="tournament__seeds__teams-list-row active">
|
||||
<li className={clsx(styles.seedsTeamsListRow, "active")}>
|
||||
<RowContents
|
||||
team={activeTeam}
|
||||
teamSeedingSkill={{
|
||||
|
|
@ -338,7 +338,7 @@ function SeedAlert({ teamOrder }: { teamOrder: number[] }) {
|
|||
const teamOrderChanged = teamOrder.some((id, i) => id !== teamOrderInDb[i]);
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post" className="tournament__seeds__form">
|
||||
<fetcher.Form method="post" className={styles.seedsForm}>
|
||||
<input type="hidden" name="tournamentId" value={tournament.ctx.id} />
|
||||
<input type="hidden" name="seeds" value={JSON.stringify(teamOrder)} />
|
||||
<input type="hidden" name="_action" value="UPDATE_SEEDS" />
|
||||
|
|
@ -382,7 +382,7 @@ function RowContents({
|
|||
<>
|
||||
<div>{seed}</div>
|
||||
<div>{logoUrl ? <Avatar url={logoUrl} size="xxs" /> : null}</div>
|
||||
<div className="tournament__seeds__team-name">
|
||||
<div className={styles.seedsTeamName}>
|
||||
{team.checkIns.length > 0 ? "✅ " : "❌ "} {team.name}
|
||||
</div>
|
||||
<div className={clsx({ "text-warning": teamSeedingSkill.outOfOrder })}>
|
||||
|
|
@ -391,10 +391,10 @@ function RowContents({
|
|||
<div className="stack horizontal sm">
|
||||
{team.members.map((member) => {
|
||||
return (
|
||||
<div key={member.userId} className="tournament__seeds__team-member">
|
||||
<div key={member.userId} className={styles.seedsTeamMember}>
|
||||
<Link
|
||||
to={userResultsPage(member, true)}
|
||||
className="tournament__seeds__team-member__name"
|
||||
className={styles.seedsTeamMemberName}
|
||||
>
|
||||
{member.username}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { TeamWithRoster } from "../components/TeamWithRoster";
|
|||
import * as Standings from "../core/Standings";
|
||||
import type { PlayedSet } from "../core/sets.server";
|
||||
import { loader } from "../loaders/to.$id.teams.$tid.server";
|
||||
import styles from "../tournament.module.css";
|
||||
import { useTournament } from "./to.$id";
|
||||
export { loader };
|
||||
|
||||
|
|
@ -86,7 +87,7 @@ export default function TournamentTeamPage() {
|
|||
teamsCount={tournament.ctx.teams.length}
|
||||
/>
|
||||
) : null}
|
||||
<div className="tournament__team__sets">
|
||||
<div className={styles.teamSets}>
|
||||
{data.sets.map((set) => {
|
||||
return <SetInfo key={set.tournamentMatchId} set={set} team={team} />;
|
||||
})}
|
||||
|
|
@ -118,46 +119,44 @@ function StatSquares({
|
|||
)?.placement;
|
||||
|
||||
return (
|
||||
<div className="tournament__team__stats">
|
||||
<div className="tournament__team__stat">
|
||||
<div className="tournament__team__stat__title">
|
||||
<div className={styles.teamStats}>
|
||||
<div className={styles.teamStat}>
|
||||
<div className={styles.teamStatTitle}>
|
||||
{t("tournament:team.setWins")}
|
||||
</div>
|
||||
<div className="tournament__team__stat__main">
|
||||
<div className={styles.teamStatMain}>
|
||||
{data.winCounts.sets.won} / {data.winCounts.sets.total}
|
||||
</div>
|
||||
<div className="tournament__team__stat__sub">
|
||||
<div className={styles.teamStatSub}>
|
||||
{data.winCounts.sets.percentage}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tournament__team__stat">
|
||||
<div className="tournament__team__stat__title">
|
||||
<div className={styles.teamStat}>
|
||||
<div className={styles.teamStatTitle}>
|
||||
{t("tournament:team.mapWins")}
|
||||
</div>
|
||||
<div className="tournament__team__stat__main">
|
||||
<div className={styles.teamStatMain}>
|
||||
{data.winCounts.maps.won} / {data.winCounts.maps.total}
|
||||
</div>
|
||||
<div className="tournament__team__stat__sub">
|
||||
<div className={styles.teamStatSub}>
|
||||
{data.winCounts.maps.percentage}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tournament__team__stat">
|
||||
<div className="tournament__team__stat__title">
|
||||
{t("tournament:team.seed")}
|
||||
</div>
|
||||
<div className="tournament__team__stat__main">{seed}</div>
|
||||
<div className="tournament__team__stat__sub">
|
||||
<div className={styles.teamStat}>
|
||||
<div className={styles.teamStatTitle}>{t("tournament:team.seed")}</div>
|
||||
<div className={styles.teamStatMain}>{seed}</div>
|
||||
<div className={styles.teamStatSub}>
|
||||
{t("tournament:team.seed.footer", { count: teamsCount })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tournament__team__stat">
|
||||
<div className="tournament__team__stat__title">
|
||||
<div className={styles.teamStat}>
|
||||
<div className={styles.teamStatTitle}>
|
||||
{t("tournament:team.placement")}
|
||||
</div>
|
||||
<div className="tournament__team__stat__main">
|
||||
<div className={styles.teamStatMain}>
|
||||
{placement ? <Placement placement={placement} textOnly /> : "-"}
|
||||
{undergroundPlacement ? (
|
||||
<>
|
||||
|
|
@ -167,12 +166,12 @@ function StatSquares({
|
|||
) : null}
|
||||
</div>
|
||||
{undergroundPlacement ? (
|
||||
<div className="tournament__team__stat__sub">
|
||||
<div className={styles.teamStatSub}>
|
||||
{t("tournament:team.placement.footer")}
|
||||
</div>
|
||||
) : null}
|
||||
{standingsResult.type === "multi" ? (
|
||||
<div className="tournament__team__stat__sub">
|
||||
<div className={styles.teamStatSub}>
|
||||
{
|
||||
standingsResult.standings.find((s) =>
|
||||
s.standings.some((s) => s.team.id === data.tournamentTeamId),
|
||||
|
|
@ -223,17 +222,15 @@ function SetInfo({ set, team }: { set: PlayedSet; team: TournamentDataTeam }) {
|
|||
tournament.matchContextNamesById(set.tournamentMatchId);
|
||||
|
||||
return (
|
||||
<div className="tournament__team__set">
|
||||
<div className="tournament__team__set__top-container">
|
||||
<div className="tournament__team__set__score">
|
||||
{set.score.join("-")}
|
||||
</div>
|
||||
<div className={styles.teamSet}>
|
||||
<div className={styles.teamSetTopContainer}>
|
||||
<div className={styles.teamSetScore}>{set.score.join("-")}</div>
|
||||
<Link
|
||||
to={tournamentMatchPage({
|
||||
matchId: set.tournamentMatchId,
|
||||
tournamentId: tournament.ctx.id,
|
||||
})}
|
||||
className="tournament__team__set__round-name"
|
||||
className={styles.teamSetRoundName}
|
||||
>
|
||||
{roundNameWithoutMatchIdentifier}{" "}
|
||||
{tournament.ctx.settings.bracketProgression.length > 1 ? (
|
||||
|
|
@ -241,7 +238,7 @@ function SetInfo({ set, team }: { set: PlayedSet; team: TournamentDataTeam }) {
|
|||
) : null}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overlap-divider">
|
||||
<div className={styles.overlapDivider}>
|
||||
<div className="stack horizontal sm">
|
||||
{set.maps.map(({ stageId, modeShort, result, source }, i) => {
|
||||
return (
|
||||
|
|
@ -252,15 +249,15 @@ function SetInfo({ set, team }: { set: PlayedSet; team: TournamentDataTeam }) {
|
|||
<ModeImage
|
||||
mode={modeShort}
|
||||
size={20}
|
||||
containerClassName={clsx("tournament__team__set__mode", {
|
||||
tournament__team__set__mode__loss: result === "loss",
|
||||
containerClassName={clsx(styles.teamSetMode, {
|
||||
[styles.teamSetModeLoss]: result === "loss",
|
||||
})}
|
||||
/>
|
||||
</SendouButton>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div className="tournament__team__set__stage-container">
|
||||
<div className={styles.teamSetStageContainer}>
|
||||
<StageImage
|
||||
stageId={stageId}
|
||||
width={125}
|
||||
|
|
@ -273,24 +270,24 @@ function SetInfo({ set, team }: { set: PlayedSet; team: TournamentDataTeam }) {
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tournament__team__set__opponent">
|
||||
<div className="tournament__team__set__opponent__vs">vs.</div>
|
||||
<div className={styles.teamSetOpponent}>
|
||||
<div className={styles.teamSetOpponentVs}>vs.</div>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentTeamId: set.opponent.id,
|
||||
tournamentId: tournament.ctx.id,
|
||||
})}
|
||||
className="tournament__team__set__opponent__team"
|
||||
className={styles.teamSetOpponentTeam}
|
||||
>
|
||||
{set.opponent.name}
|
||||
</Link>
|
||||
<div className="tournament__team__set__opponent__members">
|
||||
<div className={styles.teamSetOpponentMembers}>
|
||||
{set.opponent.roster.map((user) => {
|
||||
return (
|
||||
<Link
|
||||
to={userPage(user)}
|
||||
key={user.id}
|
||||
className="tournament__team__set__opponent__member"
|
||||
className={styles.teamSetOpponentMember}
|
||||
>
|
||||
<Avatar user={user} size="xxs" />
|
||||
{user.username}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ import { metaTags } from "../../../utils/remix";
|
|||
import { loader, type TournamentLoaderData } from "../loaders/to.$id.server";
|
||||
export { loader };
|
||||
|
||||
import "~/styles/calendar-event.css";
|
||||
import "../tournament.css";
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
||||
const navigatedToMatchPage =
|
||||
typeof args.nextParams.mid === "string" &&
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
.tournament__action-section {
|
||||
.actionSection {
|
||||
padding: var(--s-0-5) var(--s-6) var(--s-6) var(--s-6);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.tournament__action-section__top-padded {
|
||||
.actionSectionTopPadded {
|
||||
padding: var(--s-3) var(--s-6) var(--s-6) var(--s-6);
|
||||
}
|
||||
|
||||
.tournament__action-section-title {
|
||||
.actionSectionTitle {
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.tournament__action-side-note {
|
||||
.actionSideNote {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__chat-container {
|
||||
.chatContainer {
|
||||
top: var(--sticky-top);
|
||||
position: sticky;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tournament__chat-messages-container {
|
||||
.chatMessagesContainer {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.tournament__map-pool-counts {
|
||||
.mapPoolCounts {
|
||||
display: grid;
|
||||
width: 250px;
|
||||
margin: 0 auto;
|
||||
|
|
@ -40,43 +40,43 @@
|
|||
row-gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament__summary-content {
|
||||
.summaryContent {
|
||||
display: inline-flex;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.tournament__summary-content > svg {
|
||||
.summaryContent > svg {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.tournament__round-container {
|
||||
.roundContainer {
|
||||
width: 250px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tournament__select-container > label {
|
||||
.selectContainer > label {
|
||||
margin-left: var(--s-2-5);
|
||||
}
|
||||
|
||||
.tournament__teams-container {
|
||||
.teamsContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__team-select {
|
||||
.teamSelect {
|
||||
width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__bo-radios-container {
|
||||
.boRadiosContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__map-list {
|
||||
.mapList {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
column-gap: var(--s-4);
|
||||
|
|
@ -85,45 +85,45 @@
|
|||
grid-template-columns: max-content max-content;
|
||||
}
|
||||
|
||||
.tournament__pick-info {
|
||||
.pickInfo {
|
||||
align-self: center;
|
||||
font-size: var(--fonts-xxxs);
|
||||
}
|
||||
|
||||
.tournament__pick-info.team-1 {
|
||||
.pickInfoTeam1 {
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.tournament__pick-info.team-2 {
|
||||
.pickInfoTeam2 {
|
||||
color: var(--color-error-high);
|
||||
}
|
||||
|
||||
.tournament__pick-info.tiebreaker {
|
||||
.pickInfoTiebreaker {
|
||||
color: var(--color-warning-high);
|
||||
}
|
||||
|
||||
.tournament__pick-info.both {
|
||||
.pickInfoBoth {
|
||||
color: var(--color-success-high);
|
||||
}
|
||||
|
||||
.tournament__stage-listed {
|
||||
.stageListed {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster {
|
||||
.teamWithRoster {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__name {
|
||||
.teamWithRosterName {
|
||||
flex: 1;
|
||||
font-weight: var(--bold);
|
||||
padding-inline-end: var(--s-4);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__members {
|
||||
.teamWithRosterMembers {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
|
@ -135,23 +135,23 @@
|
|||
padding-block: var(--s-3);
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__member {
|
||||
.teamWithRosterMember {
|
||||
display: grid;
|
||||
gap: var(--s-1-5);
|
||||
grid-template-columns: max-content max-content 1fr;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__member__inactive {
|
||||
.teamWithRosterMemberInactive {
|
||||
text-decoration: line-through;
|
||||
color: var(--color-text-high);
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__member__avatar-inactive {
|
||||
.teamWithRosterMemberAvatarInactive {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__map-pool {
|
||||
.teamWithRosterMapPool {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content max-content max-content;
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -161,11 +161,11 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__map-pool.tournament__team-with-roster__map-pool__3-columns {
|
||||
.teamWithRosterMapPool3Columns {
|
||||
grid-template-columns: max-content max-content max-content;
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__map-pool__mode-info {
|
||||
.teamWithRosterMapPoolModeInfo {
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
font-size: var(--fonts-xxs);
|
||||
|
|
@ -176,22 +176,22 @@
|
|||
padding-block: var(--s-0-5);
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__seed {
|
||||
.teamWithRosterSeed {
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.tournament__team-with-roster__team-name {
|
||||
.teamWithRosterTeamName {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tournament__team-member-row {
|
||||
.teamMemberRow {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tournament__team-member-name {
|
||||
.teamMemberName {
|
||||
overflow: hidden;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament__team-member-name__role {
|
||||
.teamMemberNameRole {
|
||||
position: absolute;
|
||||
background-color: var(--color-text-accent);
|
||||
color: var(--color-text-inverse);
|
||||
|
|
@ -216,27 +216,27 @@
|
|||
left: 15px;
|
||||
}
|
||||
|
||||
.tournament__team-member-name__role__sub {
|
||||
.teamMemberNameRoleSub {
|
||||
background-color: var(--color-info);
|
||||
}
|
||||
|
||||
.tournament__logo-container {
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__logo {
|
||||
.logo {
|
||||
border-radius: 100%;
|
||||
min-width: 124px;
|
||||
}
|
||||
|
||||
.tournament__title {
|
||||
.title {
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.tournament__badge {
|
||||
.badge {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
|
|
@ -248,93 +248,93 @@
|
|||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament__badge__ranked {
|
||||
.badgeRanked {
|
||||
background-color: var(--color-info-low);
|
||||
color: var(--color-info-high);
|
||||
}
|
||||
|
||||
.tournament__badge__unranked {
|
||||
.badgeUnranked {
|
||||
background-color: var(--color-success-low);
|
||||
color: var(--color-success-high);
|
||||
}
|
||||
|
||||
.tournament__badge__modes {
|
||||
.badgeModes {
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
||||
.tournament__info__icon {
|
||||
.infoIcon {
|
||||
width: 18px;
|
||||
padding: var(--s-1) 0;
|
||||
}
|
||||
|
||||
.tournament__info__description {
|
||||
.infoDescription {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tournament__info__description > :is(h1, h2, h3, h4, h5, h6) {
|
||||
.infoDescription > :is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-block-end: var(--s-6);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tournament__info__description > :is(h2, h3, h4, h5, h6) {
|
||||
.infoDescription > :is(h2, h3, h4, h5, h6) {
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
|
||||
.tournament__info__description > h1 {
|
||||
.infoDescription > h1 {
|
||||
font-size: var(--fonts-xl);
|
||||
}
|
||||
|
||||
.tournament__info__description > :is(h2, h3, h4, h5, h6) {
|
||||
.infoDescription > :is(h2, h3, h4, h5, h6) {
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.tournament__info__description > :is(h3, h4, h5, h6) {
|
||||
.infoDescription > :is(h3, h4, h5, h6) {
|
||||
font-size: var(--fonts-md);
|
||||
}
|
||||
|
||||
.tournament__info__description > ul:has(+ p) {
|
||||
.infoDescription > ul:has(+ p) {
|
||||
margin-block-end: var(--s-6);
|
||||
}
|
||||
|
||||
.tournament__by {
|
||||
.by {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__section-header {
|
||||
.sectionHeader {
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.tournament__section {
|
||||
.section {
|
||||
background-color: var(--color-bg-high);
|
||||
margin-inline: -12px;
|
||||
padding: var(--s-4) var(--s-3);
|
||||
}
|
||||
|
||||
.tournament__section__input-container {
|
||||
.sectionInputContainer {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.tournament__section__warning {
|
||||
.sectionWarning {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
text-align: center;
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.tournament__section__map-select-row {
|
||||
.sectionMapSelectRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__section__icon {
|
||||
.sectionIcon {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.tournament__roster-grid {
|
||||
.rosterGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 110px);
|
||||
gap: var(--s-4);
|
||||
|
|
@ -343,14 +343,14 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.tournament__roster-grid__member-name {
|
||||
.rosterGridMemberName {
|
||||
max-width: 110px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__missing-player {
|
||||
.missingPlayer {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
font-size: 32px;
|
||||
|
|
@ -361,31 +361,31 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tournament__missing-player__optional {
|
||||
.missingPlayerOptional {
|
||||
border: 2px dashed var(--color-text-accent);
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
|
||||
.tournament__invite-container {
|
||||
.inviteContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-12);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament__seeds__form {
|
||||
.seedsForm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament__seeds__order-button {
|
||||
.seedsOrderButton {
|
||||
margin-block-start: var(--s-2);
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
/* TODO: overflow-x scroll */
|
||||
.tournament__seeds__teams-list-row {
|
||||
.seedsTeamsListRow {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
|
@ -398,29 +398,29 @@
|
|||
row-gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.tournament__seeds__teams-list-row.sortable:not(.disabled) {
|
||||
.seedsTeamsListRow.sortable:not(.disabled) {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tournament__seeds__teams-list-row.active {
|
||||
.seedsTeamsListRow.active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tournament__seeds__teams-list-row.sortable:active:not(.disabled) {
|
||||
.seedsTeamsListRow.sortable:active:not(.disabled) {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.tournament__seeds__teams-container__header {
|
||||
.seedsTeamsContainerHeader {
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.tournament__seeds__team-name {
|
||||
.seedsTeamName {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__seeds__team-member {
|
||||
.seedsTeamMember {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
grid-column-gap: var(--s-2-5);
|
||||
|
|
@ -430,27 +430,27 @@
|
|||
place-items: center;
|
||||
}
|
||||
|
||||
.tournament__seeds__teams-list-row.active .tournament__seeds__team-member {
|
||||
.seedsTeamsListRow.active .seedsTeamMember {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.tournament__seeds__team-member__name {
|
||||
.seedsTeamMemberName {
|
||||
grid-column: 1 / span 2;
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__seeds__lonely-stat {
|
||||
.seedsLonelyStat {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.tournament__seeds__plus-info {
|
||||
.seedsPlusInfo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-inline-end: var(--s-4);
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.tournament__stream__user-container {
|
||||
.streamUserContainer {
|
||||
font-size: var(--fonts-xs);
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -458,7 +458,7 @@
|
|||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__stream__viewer-count {
|
||||
.streamViewerCount {
|
||||
font-size: var(--fonts-xs);
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -467,11 +467,11 @@
|
|||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.tournament__stream__viewer-count > svg {
|
||||
.streamViewerCount > svg {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.tournament__team__stats {
|
||||
.teamStats {
|
||||
border-radius: var(--rounded);
|
||||
padding: var(--s-4);
|
||||
display: grid;
|
||||
|
|
@ -480,7 +480,7 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tournament__team__stat {
|
||||
.teamStat {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 2fr 1fr;
|
||||
|
|
@ -489,33 +489,38 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.tournament__team__stat__main {
|
||||
.teamStatTitle {
|
||||
font-size: var(--fonts-sm);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.teamStatMain {
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__team__stat__main sup {
|
||||
.teamStatMain sup {
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.tournament__team__stat__sub {
|
||||
.teamStatSub {
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.tournament__section {
|
||||
.section {
|
||||
margin: 0;
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament__team__stats {
|
||||
.teamStats {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament__team__sets {
|
||||
.teamSets {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 32rem;
|
||||
|
|
@ -523,7 +528,7 @@
|
|||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.tournament__team__set {
|
||||
.teamSet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2-5);
|
||||
|
|
@ -532,7 +537,7 @@
|
|||
padding: var(--s-3) var(--s-6);
|
||||
}
|
||||
|
||||
.tournament__team__set__top-container {
|
||||
.teamSetTopContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -542,37 +547,37 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.tournament__team__set__top-container {
|
||||
.teamSetTopContainer {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.tournament__team__set__score {
|
||||
.teamSetScore {
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--bold);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__team__set__round-name {
|
||||
.teamSetRoundName {
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--semi-bold);
|
||||
color: var(--color-text-high);
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.tournament__team__set__mode {
|
||||
.teamSetMode {
|
||||
background-color: var(--color-bg-high);
|
||||
border-radius: 100%;
|
||||
padding: var(--s-2);
|
||||
border: 2px solid var(--color-success);
|
||||
}
|
||||
|
||||
.tournament__team__set__mode__loss {
|
||||
.teamSetModeLoss {
|
||||
border-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.tournament__team__set__stage-container {
|
||||
.teamSetStageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
|
|
@ -582,7 +587,7 @@
|
|||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tournament__team__set__opponent {
|
||||
.teamSetOpponent {
|
||||
display: grid;
|
||||
grid-template-areas: "vs team" "vs members";
|
||||
grid-template-columns: max-content 1fr;
|
||||
|
|
@ -590,7 +595,7 @@
|
|||
row-gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament__team__set__opponent__vs {
|
||||
.teamSetOpponentVs {
|
||||
grid-area: vs;
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--bold);
|
||||
|
|
@ -598,21 +603,21 @@
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
.tournament__team__set__opponent__team {
|
||||
.teamSetOpponentTeam {
|
||||
grid-area: team;
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--semi-bold);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tournament__team__set__opponent__members {
|
||||
.teamSetOpponentMembers {
|
||||
grid-area: members;
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tournament__team__set__opponent__member {
|
||||
.teamSetOpponentMember {
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
gap: var(--s-1);
|
||||
|
|
@ -620,7 +625,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.overlap-divider {
|
||||
.overlapDivider {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
|
@ -628,14 +633,14 @@
|
|||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.overlap-divider::before,
|
||||
.overlap-divider::after {
|
||||
.overlapDivider::before,
|
||||
.overlapDivider::after {
|
||||
flex: 1;
|
||||
border-bottom: 2px solid var(--color-accent-low);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tournament__standings__match-result-square {
|
||||
.standingsMatchResultSquare {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
|
|
@ -647,15 +652,15 @@
|
|||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tournament__standings__match-result-square--win {
|
||||
.standingsMatchResultSquareWin {
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.tournament__standings__match-result-square--loss {
|
||||
.standingsMatchResultSquareLoss {
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
.tournament__standings__team-name {
|
||||
.standingsTeamName {
|
||||
min-width: 125px;
|
||||
word-break: break-word;
|
||||
display: flex;
|
||||
|
|
@ -664,19 +669,19 @@
|
|||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tournament__standings__divider {
|
||||
.standingsDivider {
|
||||
width: 5px;
|
||||
background-color: var(--color-accent-low);
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament__div__grid {
|
||||
.divGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__div__link {
|
||||
.divLink {
|
||||
background-color: var(--color-bg-high);
|
||||
padding: var(--s-2) var(--s-4);
|
||||
border-radius: var(--rounded);
|
||||
|
|
@ -685,21 +690,21 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tournament__div__link:focus-visible {
|
||||
.divLink:focus-visible {
|
||||
outline: 3px solid var(--color-accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.tournament__div__link__participant {
|
||||
.divLinkParticipant {
|
||||
outline: 3px solid var(--color-bg-higher);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.tournament__div__link__participant svg {
|
||||
.divLinkParticipant svg {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.tournament__div__participant-counts {
|
||||
.divParticipantCounts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-high);
|
||||
|
|
@ -708,35 +713,35 @@
|
|||
margin-block-end: var(--s-1);
|
||||
}
|
||||
|
||||
.tournament__div__participant-counts > svg {
|
||||
.divParticipantCounts > svg {
|
||||
width: 1rem;
|
||||
margin-block-end: 1px;
|
||||
}
|
||||
|
||||
.tournament__standings__divider-row {
|
||||
.standingsDividerRow {
|
||||
background: transparent !important;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tournament__standings__divider {
|
||||
.standingsDividerPadding {
|
||||
padding-block: var(--s-2);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tournament__standings__divider-content {
|
||||
.standingsDividerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.tournament__standings__divider-line {
|
||||
.standingsDividerLine {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament__standings__divider-text {
|
||||
.standingsDividerText {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
text-transform: uppercase;
|
||||
|
|
@ -746,24 +751,20 @@
|
|||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.tournament__standings__divider--qualified
|
||||
.tournament__standings__divider-line {
|
||||
.standingsDividerQualifiedLine {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.tournament__standings__divider--qualified
|
||||
.tournament__standings__divider-text {
|
||||
.standingsDividerQualifiedText {
|
||||
color: var(--color-success);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.tournament__standings__divider--eliminated
|
||||
.tournament__standings__divider-line {
|
||||
.standingsDividerEliminatedLine {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.tournament__standings__divider--eliminated
|
||||
.tournament__standings__divider-text {
|
||||
.standingsDividerEliminatedText {
|
||||
color: var(--color-error);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import type { UserResultsLoaderData } from "../loaders/u.$identifier.results.server";
|
||||
import styles from "../user-page.module.css";
|
||||
import { ParticipationPill } from "./ParticipationPill";
|
||||
|
||||
export type UserResultsTableProps = {
|
||||
|
|
@ -139,7 +140,7 @@ export function UserResultsTable({
|
|||
}
|
||||
>
|
||||
<ul
|
||||
className="u__results-players"
|
||||
className={styles.resultsPlayers}
|
||||
data-testid={`mates-cell-placement-${i}`}
|
||||
>
|
||||
{result.mates.map((player) => (
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import { loader } from "../loaders/u.$identifier.builds.new.server";
|
|||
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
||||
export { loader, action };
|
||||
|
||||
import { mainStyles } from "~/components/Main";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "gear"],
|
||||
};
|
||||
|
|
@ -63,7 +65,7 @@ export default function NewBuildPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="half-width u__build-form">
|
||||
<div className={mainStyles.narrow}>
|
||||
<Form className="stack md items-start" method="post">
|
||||
{buildToEdit && (
|
||||
<input type="hidden" name="buildToEditId" value={buildToEdit.id} />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
|||
import { DEFAULT_BUILD_SORT } from "../user-page-constants";
|
||||
export { loader, action };
|
||||
|
||||
import userStyles from "../user-page.module.css";
|
||||
import styles from "./u.$identifier.builds.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -142,7 +143,7 @@ function BuildsFilters({
|
|||
onPress={() => setWeaponFilter("ALL")}
|
||||
variant={weaponFilter === "ALL" ? undefined : "outlined"}
|
||||
size="small"
|
||||
className="u__build-filter-button"
|
||||
className={userStyles.buildFilterButton}
|
||||
>
|
||||
{t("builds:stats.all")} ({data.builds.length})
|
||||
</SendouButton>
|
||||
|
|
@ -152,7 +153,7 @@ function BuildsFilters({
|
|||
onPress={() => setWeaponFilter("PUBLIC")}
|
||||
variant={weaponFilter === "PUBLIC" ? undefined : "outlined"}
|
||||
size="small"
|
||||
className="u__build-filter-button"
|
||||
className={userStyles.buildFilterButton}
|
||||
icon={<UnlockIcon />}
|
||||
>
|
||||
{t("builds:stats.public")} ({publicBuildsCount})
|
||||
|
|
@ -161,7 +162,7 @@ function BuildsFilters({
|
|||
onPress={() => setWeaponFilter("PRIVATE")}
|
||||
variant={weaponFilter === "PRIVATE" ? undefined : "outlined"}
|
||||
size="small"
|
||||
className="u__build-filter-button"
|
||||
className={userStyles.buildFilterButton}
|
||||
icon={<LockIcon />}
|
||||
>
|
||||
{t("builds:stats.private")} ({privateBuildsCount})
|
||||
|
|
@ -338,7 +339,7 @@ function WeaponFilterMenu({
|
|||
<SendouButton
|
||||
variant={typeof weaponFilter === "number" ? undefined : "outlined"}
|
||||
size="small"
|
||||
className="u__build-filter-button"
|
||||
className={userStyles.buildFilterButton}
|
||||
>
|
||||
<Image
|
||||
path={weaponCategoryUrl("SHOOTERS")}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
|||
import { COUNTRY_CODES, USER } from "../user-page-constants";
|
||||
export { loader, action };
|
||||
|
||||
import styles from "~/styles/u.$identifier.module.css";
|
||||
import { mainStyles } from "~/components/Main";
|
||||
import styles from "../user-page.module.css";
|
||||
import editStyles from "./u.$identifier.edit.module.css";
|
||||
|
||||
export default function UserEditPage() {
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
|
|
@ -44,8 +46,8 @@ export default function UserEditPage() {
|
|||
const isArtist = useHasRole("ARTIST");
|
||||
|
||||
return (
|
||||
<div className="half-width">
|
||||
<Form className={styles.container} method="post">
|
||||
<div className={mainStyles.narrow}>
|
||||
<Form className={editStyles.container} method="post">
|
||||
{isSupporter ? (
|
||||
<CustomizedColorsInput initialColors={layoutData.css} />
|
||||
) : null}
|
||||
|
|
@ -148,9 +150,9 @@ function InGameNameInputs() {
|
|||
maxLength={USER.IN_GAME_NAME_TEXT_MAX_LENGTH}
|
||||
defaultValue={inGameNameParts?.[0]}
|
||||
/>
|
||||
<div className={styles.inGameNameHashtag}>#</div>
|
||||
<div className={editStyles.inGameNameHashtag}>#</div>
|
||||
<Input
|
||||
className={styles.inGameNameDiscriminator}
|
||||
className={editStyles.inGameNameDiscriminator}
|
||||
name="inGameNameDiscriminator"
|
||||
aria-label="In game name discriminator"
|
||||
maxLength={USER.IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH}
|
||||
|
|
@ -171,14 +173,14 @@ function SensSelects() {
|
|||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className={styles.sensContainer}>
|
||||
<div className={editStyles.sensContainer}>
|
||||
<div>
|
||||
<Label htmlFor="motionSens">{t("user:motionSens")}</Label>
|
||||
<select
|
||||
id="motionSens"
|
||||
name="motionSens"
|
||||
defaultValue={data.user.motionSens ?? undefined}
|
||||
className={styles.sensSelect}
|
||||
className={editStyles.sensSelect}
|
||||
>
|
||||
<option value="">{"-"}</option>
|
||||
{SENS_OPTIONS.map((sens) => (
|
||||
|
|
@ -195,7 +197,7 @@ function SensSelects() {
|
|||
id="stickSens"
|
||||
name="stickSens"
|
||||
defaultValue={data.user.stickSens ?? undefined}
|
||||
className={styles.sensSelect}
|
||||
className={editStyles.sensSelect}
|
||||
>
|
||||
<option value="">{"-"}</option>
|
||||
{SENS_OPTIONS.map((sens) => (
|
||||
|
|
@ -280,7 +282,7 @@ function WeaponPoolSelect() {
|
|||
const latestWeapon = weapons[weapons.length - 1];
|
||||
|
||||
return (
|
||||
<div className={clsx("stack md", styles.weaponPool)}>
|
||||
<div className={clsx("stack md", editStyles.weaponPool)}>
|
||||
<input type="hidden" name="weapons" value={JSON.stringify(weapons)} />
|
||||
{weapons.length < USER.WEAPON_POOL_MAX_SIZE ? (
|
||||
<WeaponSelect
|
||||
|
|
@ -307,7 +309,7 @@ function WeaponPoolSelect() {
|
|||
{weapons.map((weapon) => {
|
||||
return (
|
||||
<div key={weapon.weaponSplId} className="stack xs items-center">
|
||||
<div className="u__weapon">
|
||||
<div className={styles.weapon}>
|
||||
<WeaponImage
|
||||
weaponSplId={weapon.weaponSplId}
|
||||
variant={weapon.isFavorite ? "badge-5-star" : "badge"}
|
||||
|
|
@ -365,7 +367,7 @@ function BioTextarea({
|
|||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
|
||||
return (
|
||||
<div className={styles.bioContainer}>
|
||||
<div className={editStyles.bioContainer}>
|
||||
<Label
|
||||
htmlFor="bio"
|
||||
valueLimits={{ current: value.length, max: USER.BIO_MAX_LENGTH }}
|
||||
|
|
@ -487,7 +489,7 @@ function CommissionTextArea({
|
|||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
|
||||
return (
|
||||
<div className={styles.bioContainer}>
|
||||
<div className={editStyles.bioContainer}>
|
||||
<Label
|
||||
htmlFor="commissionText"
|
||||
valueLimits={{
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import { loader } from "../loaders/u.$identifier.index.server";
|
||||
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
||||
import styles from "../user-page.module.css";
|
||||
export { loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -39,11 +40,11 @@ export default function UserInfoPage() {
|
|||
const layoutData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<div className="u__container">
|
||||
<div className="u__avatar-container">
|
||||
<Avatar user={layoutData.user} size="lg" className="u__avatar" />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar user={layoutData.user} size="lg" className={styles.avatar} />
|
||||
<div>
|
||||
<h2 className="u__name">
|
||||
<h2 className={styles.name}>
|
||||
<div>{layoutData.user.username}</div>
|
||||
<div>
|
||||
{data.user.country ? (
|
||||
|
|
@ -53,7 +54,7 @@ export default function UserInfoPage() {
|
|||
</h2>
|
||||
<TeamInfo />
|
||||
</div>
|
||||
<div className="u__socials">
|
||||
<div className={styles.socials}>
|
||||
{data.user.twitch ? (
|
||||
<SocialLink type="twitch" identifier={data.user.twitch} />
|
||||
) : null}
|
||||
|
|
@ -87,7 +88,7 @@ function TeamInfo() {
|
|||
<div className="stack horizontal sm">
|
||||
<Link
|
||||
to={teamPage(data.user.team.customUrl)}
|
||||
className="u__team"
|
||||
className={styles.team}
|
||||
data-testid="main-team-link"
|
||||
>
|
||||
{data.user.team.avatarUrl ? (
|
||||
|
|
@ -145,7 +146,7 @@ function SecondaryTeamsPopover() {
|
|||
>
|
||||
<Link
|
||||
to={teamPage(team.customUrl)}
|
||||
className="u__team text-main-forced"
|
||||
className={clsx(styles.team, "text-main-forced")}
|
||||
>
|
||||
{team.avatarUrl ? (
|
||||
<img
|
||||
|
|
@ -199,11 +200,11 @@ export function SocialLink({
|
|||
|
||||
return (
|
||||
<a
|
||||
className={clsx("u__social-link", {
|
||||
youtube: type === "youtube",
|
||||
twitch: type === "twitch",
|
||||
battlefy: type === "battlefy",
|
||||
bsky: type === "bsky",
|
||||
className={clsx(styles.socialLink, {
|
||||
[styles.socialLinkYoutube]: type === "youtube",
|
||||
[styles.socialLinkTwitch]: type === "twitch",
|
||||
[styles.socialLinkBattlefy]: type === "battlefy",
|
||||
[styles.socialLinkBsky]: type === "bsky",
|
||||
})}
|
||||
href={href()}
|
||||
>
|
||||
|
|
@ -251,30 +252,30 @@ function ExtraInfos() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="u__extra-infos">
|
||||
<div className="u__extra-info">#{data.user.id}</div>
|
||||
<div className={styles.extraInfos}>
|
||||
<div className={styles.extraInfo}>#{data.user.id}</div>
|
||||
{data.user.discordUniqueName && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">
|
||||
<div className={styles.extraInfo}>
|
||||
<span className={styles.extraInfoHeading}>
|
||||
<DiscordIcon />
|
||||
</span>{" "}
|
||||
{data.user.discordUniqueName}
|
||||
</div>
|
||||
)}
|
||||
{data.user.inGameName && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">{t("user:ign.short")}</span>{" "}
|
||||
<div className={styles.extraInfo}>
|
||||
<span className={styles.extraInfoHeading}>{t("user:ign.short")}</span>{" "}
|
||||
{data.user.inGameName}
|
||||
</div>
|
||||
)}
|
||||
{typeof data.user.stickSens === "number" && (
|
||||
<div className="u__extra-info">
|
||||
<span className="u__extra-info__heading">{t("user:sens")}</span>{" "}
|
||||
<div className={styles.extraInfo}>
|
||||
<span className={styles.extraInfoHeading}>{t("user:sens")}</span>{" "}
|
||||
{[motionSensText, stickSensText].filter(Boolean).join(" / ")}
|
||||
</div>
|
||||
)}
|
||||
{data.user.plusTier && (
|
||||
<div className="u__extra-info">
|
||||
<div className={styles.extraInfo}>
|
||||
<Image path={navIconUrl("plus")} width={20} height={20} alt="" />{" "}
|
||||
{data.user.plusTier}
|
||||
</div>
|
||||
|
|
@ -292,7 +293,7 @@ function WeaponPool() {
|
|||
<div className="stack horizontal sm justify-center">
|
||||
{data.user.weapons.map((weapon, i) => {
|
||||
return (
|
||||
<div key={weapon.weaponSplId} className="u__weapon">
|
||||
<div key={weapon.weaponSplId} className={styles.weapon}>
|
||||
<WeaponImage
|
||||
testId={`${weapon.weaponSplId}-${i + 1}`}
|
||||
weaponSplId={weapon.weaponSplId}
|
||||
|
|
@ -315,7 +316,7 @@ function TopPlacements() {
|
|||
return (
|
||||
<Link
|
||||
to={topSearchPlayerPage(data.user.topPlacements[0].playerId)}
|
||||
className="u__placements"
|
||||
className={styles.placements}
|
||||
data-testid="placements-box"
|
||||
>
|
||||
{modesShort.map((mode) => {
|
||||
|
|
@ -326,7 +327,7 @@ function TopPlacements() {
|
|||
if (!placement) return null;
|
||||
|
||||
return (
|
||||
<div key={mode} className="u__placements__mode">
|
||||
<div key={mode} className={styles.placementsMode}>
|
||||
<Image path={modeImageUrl(mode)} alt="" width={24} height={24} />
|
||||
<div>
|
||||
{placement.rank} / {placement.power}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user