Refactor several to CSS Modules

This commit is contained in:
Kalle 2025-12-30 16:17:24 +02:00
parent 4c0ff39da2
commit 0193edce1d
107 changed files with 2054 additions and 2015 deletions

View 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);
}
}

View File

@ -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;
};

View File

@ -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>
}
>

View 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);
}
}

View File

@ -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>

View File

@ -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")
) : (

View 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;
}
}

View File

@ -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="" />

View File

@ -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, {

View 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;
}
}

View File

@ -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>
);
}

View 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);
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>
);

View File

@ -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);

View File

@ -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" ? (

View File

@ -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,
})}
>

View File

@ -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")}

View File

@ -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={() =>

View File

@ -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;

View File

@ -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>
)}

View File

@ -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;
}

View File

@ -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`,
}}

View File

@ -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);
}

View File

@ -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%;

View File

@ -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,

View File

@ -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 {

View File

@ -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>

View File

@ -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>
)}

View File

@ -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 = {

View File

@ -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;

View File

@ -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>
)}

View File

@ -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>
);

View 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;
}

View File

@ -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}

View File

@ -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;
}

View File

@ -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({

View File

@ -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);

View File

@ -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>

View File

@ -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);
}

View File

@ -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}

View File

@ -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"

View File

@ -9,7 +9,7 @@ import {
plusSuggestionsNewPage,
} from "~/utils/urls";
import "~/styles/plus.css";
import "../plus.module.css";
export const handle: SendouRouteHandle = {
navItemName: "plus",

View File

@ -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);

View File

@ -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&apos;t pass</span>
<span className={styles.fail}>didn&apos;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>

View File

@ -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>

View File

@ -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;

View File

@ -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">

View File

@ -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)}

View File

@ -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

View File

@ -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}

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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, {

View File

@ -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;
}
}

View File

@ -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}/

View File

@ -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;

View File

@ -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"),

View File

@ -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;
}

View File

@ -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 ||

View File

@ -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>

View File

@ -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>

View File

@ -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",

View File

@ -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;

View File

@ -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,
},
)}

View File

@ -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;
}

View File

@ -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}

View File

@ -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

View File

@ -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}

View File

@ -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>
);

View File

@ -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 ? (

View File

@ -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>

View File

@ -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"

View File

@ -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}

View File

@ -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"}

View File

@ -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

View File

@ -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);

View File

@ -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"

View File

@ -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} />

View File

@ -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"

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}>

View File

@ -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"

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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" &&

View File

@ -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;
}

View File

@ -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) => (

View File

@ -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} />

View File

@ -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")}

View File

@ -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={{

View File

@ -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