mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Merge branch 'css-rework-sidenav' of https://github.com/sendou-ink/sendou.ink into css-rework-sidenav
This commit is contained in:
commit
26ceb883e2
8
app/components/EventsList.module.css
Normal file
8
app/components/EventsList.module.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.dayHeader {
|
||||
padding: var(--s-2) var(--s-2) var(--s-1);
|
||||
font-size: var(--font-2xs);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-text-high);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
101
app/components/EventsList.tsx
Normal file
101
app/components/EventsList.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { isToday, isTomorrow } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SidebarEvent } from "~/features/sidebar/core/sidebar.server";
|
||||
import styles from "./EventsList.module.css";
|
||||
import { ListLink } from "./SideNav";
|
||||
|
||||
export function EventsList({
|
||||
events,
|
||||
onClick,
|
||||
}: {
|
||||
events: SidebarEvent[];
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["front"]);
|
||||
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<div className="text-lighter text-sm p-2">
|
||||
{t("front:sideNav.noEvents")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getDayKey = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toDateString();
|
||||
};
|
||||
|
||||
const formatDayHeader = (date: Date) => {
|
||||
if (isToday(date)) {
|
||||
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: "auto",
|
||||
});
|
||||
const str = rtf.format(0, "day");
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
if (isTomorrow(date)) {
|
||||
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: "auto",
|
||||
});
|
||||
const str = rtf.format(1, "day");
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
return date.toLocaleDateString(i18n.language, {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString(i18n.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const groupedEvents = events.reduce<Record<string, typeof events>>(
|
||||
(acc, event) => {
|
||||
const key = getDayKey(event.startTime);
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(event);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const dayKeys = Object.keys(groupedEvents);
|
||||
|
||||
return (
|
||||
<>
|
||||
{dayKeys.map((dayKey) => {
|
||||
const dayEvents = groupedEvents[dayKey];
|
||||
const firstDate = new Date(dayEvents[0].startTime * 1000);
|
||||
|
||||
return (
|
||||
<div key={dayKey}>
|
||||
<div className={styles.dayHeader}>{formatDayHeader(firstDate)}</div>
|
||||
{dayEvents.map((event) => (
|
||||
<ListLink
|
||||
key={`${event.type}-${event.id}`}
|
||||
to={event.url}
|
||||
imageUrl={event.logoUrl ?? undefined}
|
||||
subtitle={formatTime(new Date(event.startTime * 1000))}
|
||||
onClick={onClick}
|
||||
>
|
||||
{event.scrimStatus === "booked"
|
||||
? t("front:sideNav.scrimVs", { opponent: event.name })
|
||||
: event.scrimStatus === "looking"
|
||||
? t("front:sideNav.lookingForScrim")
|
||||
: event.name}
|
||||
</ListLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ export function GearSelect<Clearable extends boolean | undefined = undefined>({
|
|||
>
|
||||
{({ key, items: gear, brandId, idx }) => (
|
||||
<SendouSelectItemSection
|
||||
className={idx === 0 ? "pt-0-5-forced" : undefined}
|
||||
className={idx === 0 ? "pt-0-5" : undefined}
|
||||
heading={t(`game-misc:BRAND_${brandId}` as any)}
|
||||
headingImgPath={brandImageUrl(brandId)}
|
||||
key={key}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const Main = ({
|
|||
classNameOverwrite
|
||||
? clsx(classNameOverwrite, {
|
||||
[styles.narrow]: halfWidth,
|
||||
"pt-8-forced": showLeaderboard,
|
||||
"pt-8": showLeaderboard,
|
||||
})
|
||||
: clsx(
|
||||
styles.main,
|
||||
|
|
@ -40,7 +40,7 @@ export const Main = ({
|
|||
{
|
||||
[styles.narrow]: halfWidth,
|
||||
[styles.wide]: bigger,
|
||||
"pt-8-forced": showLeaderboard,
|
||||
"pt-8": showLeaderboard,
|
||||
},
|
||||
className,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export function Markdown({ children }: { children: string }) {
|
|||
.replace(/style\s*=\s*("[^"]*"|'[^']*')/gi, (_match, value) => {
|
||||
const sanitized = value.replace(CSS_URL_REGEX, "");
|
||||
return `style=${sanitized}`;
|
||||
});
|
||||
})
|
||||
.replace(/ +$/gm, "");
|
||||
|
||||
return (
|
||||
<MarkdownToJsx
|
||||
|
|
|
|||
|
|
@ -77,40 +77,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.notificationDot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--color-text-accent);
|
||||
border-radius: 100%;
|
||||
outline: 2px solid var(--color-bg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notificationDotPulse {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-text-accent);
|
||||
box-shadow: 0 0 0 var(--color-text-accent);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
box-shadow: 0 0 0 0 var(--color-text-accent);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.panelOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
|
@ -333,24 +299,24 @@
|
|||
}
|
||||
|
||||
.panelSectionLink {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
width: fit-content;
|
||||
margin: var(--s-4) auto;
|
||||
font-size: var(--font-2xs);
|
||||
color: var(--color-text-high);
|
||||
text-decoration: none;
|
||||
padding: var(--s-3) var(--s-2);
|
||||
text-align: center;
|
||||
padding: var(--s-1) var(--s-3);
|
||||
background-color: var(--color-bg-higher);
|
||||
border-radius: var(--radius-field);
|
||||
|
||||
& svg {
|
||||
stroke-width: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.panelSectionLink:hover {
|
||||
color: var(--color-text);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dayHeader {
|
||||
padding: var(--s-2) var(--s-2) var(--s-1);
|
||||
font-size: var(--font-2xs);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-text-high);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from "clsx";
|
||||
import { isToday, isTomorrow } from "date-fns";
|
||||
import {
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
LogIn,
|
||||
Menu,
|
||||
Settings,
|
||||
|
|
@ -18,12 +18,14 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { FriendMenu } from "~/features/friends/components/FriendMenu";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import {
|
||||
EVENTS_PAGE,
|
||||
FRIENDS_PAGE,
|
||||
navIconUrl,
|
||||
SETTINGS_PAGE,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { EventsList } from "./EventsList";
|
||||
import { SendouButton } from "./elements/Button";
|
||||
import { Image } from "./Image";
|
||||
import { LogInButtonContainer } from "./layout/LogInButtonContainer";
|
||||
|
|
@ -33,7 +35,7 @@ import {
|
|||
} from "./layout/NotificationPopover";
|
||||
import { navItems } from "./layout/nav-items";
|
||||
import styles from "./MobileNav.module.css";
|
||||
import { ListLink } from "./SideNav";
|
||||
import { NotificationDot } from "./NotificationDot";
|
||||
import { StreamListItems } from "./StreamListItems";
|
||||
|
||||
type SidebarData = RootLoaderData["sidebar"] | undefined;
|
||||
|
|
@ -169,11 +171,7 @@ function MobileTab({
|
|||
>
|
||||
<span className={styles.tabIcon}>
|
||||
{icon}
|
||||
{showNotificationDot ? (
|
||||
<span className={styles.notificationDot}>
|
||||
<span className={styles.notificationDotPulse} />
|
||||
</span>
|
||||
) : null}
|
||||
{showNotificationDot ? <NotificationDot /> : null}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
|
|
@ -293,7 +291,7 @@ function FriendsPanel({
|
|||
<FriendMenu key={friend.id} {...friend} onNavigate={onClose} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-lighter text-sm p-2">
|
||||
<div className="text-lighter text-sm p-2 text-center">
|
||||
{user
|
||||
? t("front:sideNav.friends.noFriends")
|
||||
: t("front:sideNav.friends.notLoggedIn")}
|
||||
|
|
@ -305,6 +303,7 @@ function FriendsPanel({
|
|||
onClick={onClose}
|
||||
>
|
||||
{t("common:actions.viewAll")}
|
||||
<ChevronRight size={14} />
|
||||
</Link>
|
||||
</MobilePanel>
|
||||
);
|
||||
|
|
@ -317,91 +316,19 @@ function TourneysPanel({
|
|||
events: NonNullable<SidebarData>["events"];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["front"]);
|
||||
|
||||
const formatDayHeader = (date: Date) => {
|
||||
if (isToday(date)) {
|
||||
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: "auto",
|
||||
});
|
||||
const str = rtf.format(0, "day");
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
if (isTomorrow(date)) {
|
||||
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: "auto",
|
||||
});
|
||||
const str = rtf.format(1, "day");
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
return date.toLocaleDateString(i18n.language, {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString(i18n.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const getDayKey = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toDateString();
|
||||
};
|
||||
|
||||
const groupedEvents = events.reduce<Record<string, typeof events>>(
|
||||
(acc, event) => {
|
||||
const key = getDayKey(event.startTime);
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(event);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const dayKeys = Object.keys(groupedEvents);
|
||||
const { t } = useTranslation(["front", "common"]);
|
||||
|
||||
return (
|
||||
<MobilePanel title={t("front:sideNav.myCalendar")} onClose={onClose}>
|
||||
{events.length > 0 ? (
|
||||
dayKeys.map((dayKey) => {
|
||||
const dayEvents = groupedEvents[dayKey];
|
||||
const firstDate = new Date(dayEvents[0].startTime * 1000);
|
||||
|
||||
return (
|
||||
<div key={dayKey}>
|
||||
<div className={styles.dayHeader}>
|
||||
{formatDayHeader(firstDate)}
|
||||
</div>
|
||||
{dayEvents.map((event) => (
|
||||
<ListLink
|
||||
key={`${event.type}-${event.id}`}
|
||||
to={event.url}
|
||||
imageUrl={event.logoUrl ?? undefined}
|
||||
subtitle={formatTime(new Date(event.startTime * 1000))}
|
||||
onClick={onClose}
|
||||
>
|
||||
{event.scrimStatus === "booked"
|
||||
? t("front:sideNav.scrimVs", { opponent: event.name })
|
||||
: event.scrimStatus === "looking"
|
||||
? t("front:sideNav.lookingForScrim")
|
||||
: event.name}
|
||||
</ListLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-lighter text-sm p-2">
|
||||
{t("front:sideNav.noEvents")}
|
||||
</div>
|
||||
)}
|
||||
<EventsList events={events} onClick={onClose} />
|
||||
<Link
|
||||
to={EVENTS_PAGE}
|
||||
className={styles.panelSectionLink}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:actions.viewAll")}
|
||||
<ChevronRight size={14} />
|
||||
</Link>
|
||||
</MobilePanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
33
app/components/NotificationDot.module.css
Normal file
33
app/components/NotificationDot.module.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.dot {
|
||||
position: absolute;
|
||||
top: var(--dot-top, -2px);
|
||||
right: var(--dot-right, -2px);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--color-text-accent);
|
||||
border-radius: 100%;
|
||||
outline: 2px solid var(--color-bg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-text-accent);
|
||||
box-shadow: 0 0 0 var(--color-text-accent);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
box-shadow: 0 0 0 0 var(--color-text-accent);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
10
app/components/NotificationDot.tsx
Normal file
10
app/components/NotificationDot.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import clsx from "clsx";
|
||||
import styles from "./NotificationDot.module.css";
|
||||
|
||||
export function NotificationDot({ className }: { className?: string }) {
|
||||
return (
|
||||
<span className={clsx(styles.dot, className)}>
|
||||
<span className={styles.pulse} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
align-items: center;
|
||||
padding-inline: var(--s-2);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sideNavTopCentered {
|
||||
|
|
@ -121,35 +122,8 @@
|
|||
}
|
||||
|
||||
.sideNavFooterUnseenDot {
|
||||
background-color: var(--color-text-accent);
|
||||
border-radius: 100%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
outline: 2px solid var(--color-bg-high);
|
||||
}
|
||||
|
||||
.sideNavFooterUnseenDotPulse {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-text-accent);
|
||||
box-shadow: 0 0 0 var(--color-text-accent);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
box-shadow: 0 0 0 0 inherit;
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
--dot-top: 2px;
|
||||
--dot-right: 2px;
|
||||
}
|
||||
|
||||
.sideNavHeader {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,12 @@ export function ListLink({
|
|||
{subtitle || badge ? (
|
||||
<div className={styles.listLinkSubtitleRow}>
|
||||
{subtitle ? (
|
||||
<span className={styles.listLinkSubtitle}>{subtitle}</span>
|
||||
<span
|
||||
className={styles.listLinkSubtitle}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
{typeof badge === "string" ? (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { isToday, isTomorrow } from "date-fns";
|
|||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SidebarStream } from "~/features/core/streams/streams.server";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { LanguageCode } from "~/modules/i18n/config";
|
||||
import { databaseTimestampToDate, formatDistanceToNow } from "~/utils/dates";
|
||||
import { navIconUrl } from "~/utils/urls";
|
||||
|
|
@ -19,7 +18,6 @@ export function StreamListItems({
|
|||
onClick?: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["front"]);
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const formatRelativeDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
|
@ -60,7 +58,7 @@ export function StreamListItems({
|
|||
const prevIsLive =
|
||||
prevStream &&
|
||||
databaseTimestampToDate(prevStream.startsAt).getTime() <= Date.now();
|
||||
const showUpcomingDivider = isMounted && isUpcoming && prevIsLive;
|
||||
const showUpcomingDivider = isUpcoming && prevIsLive;
|
||||
|
||||
return (
|
||||
<React.Fragment key={stream.id}>
|
||||
|
|
@ -85,22 +83,16 @@ export function StreamListItems({
|
|||
</span>
|
||||
) : stream.subtitle ? (
|
||||
stream.subtitle
|
||||
) : isMounted ? (
|
||||
isUpcoming ? (
|
||||
formatRelativeDate(stream.startsAt)
|
||||
) : (
|
||||
formatDistanceToNow(startsAtDate, {
|
||||
addSuffix: true,
|
||||
language: i18n.language as LanguageCode,
|
||||
})
|
||||
)
|
||||
) : isUpcoming ? (
|
||||
formatRelativeDate(stream.startsAt)
|
||||
) : (
|
||||
""
|
||||
formatDistanceToNow(startsAtDate, {
|
||||
addSuffix: true,
|
||||
language: i18n.language as LanguageCode,
|
||||
})
|
||||
)
|
||||
}
|
||||
badge={
|
||||
isMounted && !isUpcoming ? "LIVE" : streamTierBadge(stream)
|
||||
}
|
||||
badge={!isUpcoming ? "LIVE" : streamTierBadge(stream)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{stream.name}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export function WeaponSelect<
|
|||
? specialWeaponImageUrl(TRIZOOKA_ID)
|
||||
: weaponCategoryUrl(name)
|
||||
}
|
||||
className={idx === 0 ? "pt-0-5-forced" : undefined}
|
||||
className={idx === 0 ? "pt-0-5" : undefined}
|
||||
key={key}
|
||||
>
|
||||
{weapons.map(({ weapon, name }) => (
|
||||
|
|
|
|||
|
|
@ -44,3 +44,32 @@
|
|||
.divider {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.viewAllLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
width: fit-content;
|
||||
margin: var(--s-2) auto;
|
||||
font-size: var(--font-2xs);
|
||||
color: var(--color-text-high);
|
||||
text-decoration: none;
|
||||
padding: var(--s-1) var(--s-3);
|
||||
background-color: var(--color-bg-higher);
|
||||
border-radius: var(--radius-field);
|
||||
|
||||
& svg {
|
||||
stroke-width: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
.viewAllLink {
|
||||
margin-top: var(--s-4);
|
||||
}
|
||||
}
|
||||
|
||||
.viewAllLink:hover {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-high);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Bell, RefreshCcw } from "lucide-react";
|
||||
import { Bell, ChevronRight, RefreshCcw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMatches, useRevalidator } from "react-router";
|
||||
import { Link, useMatches, useRevalidator } from "react-router";
|
||||
import {
|
||||
NotificationItem,
|
||||
NotificationItemDivider,
|
||||
|
|
@ -11,7 +11,7 @@ import { NOTIFICATIONS } from "~/features/notifications/notifications-contants";
|
|||
import type { RootLoaderData } from "~/root";
|
||||
import { NOTIFICATIONS_URL } from "~/utils/urls";
|
||||
import { useMarkNotificationsAsSeen } from "../../features/notifications/notifications-hooks";
|
||||
import { LinkButton, SendouButton } from "../elements/Button";
|
||||
import { SendouButton } from "../elements/Button";
|
||||
|
||||
import styles from "./NotificationPopover.module.css";
|
||||
|
||||
|
|
@ -95,15 +95,15 @@ function NotificationsFooter({ onClose }: { onClose?: () => void }) {
|
|||
return (
|
||||
<div>
|
||||
<hr className={styles.divider} />
|
||||
<LinkButton
|
||||
variant="minimal"
|
||||
size="small"
|
||||
<Link
|
||||
to={NOTIFICATIONS_URL}
|
||||
testId="notifications-see-all-button"
|
||||
className={styles.viewAllLink}
|
||||
data-testid="notifications-see-all-button"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:notifications.seeAll")}
|
||||
</LinkButton>
|
||||
{t("common:actions.viewAll")}
|
||||
<ChevronRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,18 @@
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
.siteTitleFlipper {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.siteTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.siteLogo {
|
||||
|
|
@ -101,6 +107,7 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
animation: fadeInFull 200ms ease-out 150ms both;
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +142,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sideNavCollapseButtonContainer {
|
||||
position: relative;
|
||||
--dot-top: 2px;
|
||||
--dot-right: 2px;
|
||||
}
|
||||
|
||||
/* Doubled selector increases specificity to beat Button.module.css's display: flex */
|
||||
.sideNavCollapseButton.sideNavCollapseButton,
|
||||
.sideNavModalTrigger.sideNavModalTrigger {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,18 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { FriendMenu } from "~/features/friends/components/FriendMenu";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { FRIENDS_PAGE, SETTINGS_PAGE, userPage } from "~/utils/urls";
|
||||
import {
|
||||
EVENTS_PAGE,
|
||||
FRIENDS_PAGE,
|
||||
SETTINGS_PAGE,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { SendouButton } from "../elements/Button";
|
||||
import { SendouPopover } from "../elements/Popover";
|
||||
import { Image } from "../Image";
|
||||
import { MobileNav } from "../MobileNav";
|
||||
import { NotificationDot } from "../NotificationDot";
|
||||
import { ListLink, SideNav, SideNavFooter, SideNavHeader } from "../SideNav";
|
||||
import sideNavStyles from "../SideNav.module.css";
|
||||
import { StreamListItems } from "../StreamListItems";
|
||||
|
|
@ -188,6 +194,7 @@ export function Layout({
|
|||
}, []);
|
||||
|
||||
const user = useUser();
|
||||
const { unseenIds } = useNotifications();
|
||||
const sidebarData = data?.sidebar;
|
||||
const events = sidebarData?.events ?? [];
|
||||
const friends = sidebarData?.friends ?? [];
|
||||
|
|
@ -208,7 +215,17 @@ export function Layout({
|
|||
|
||||
const sideNavChildren = (
|
||||
<>
|
||||
<SideNavHeader icon={<Calendar />}>
|
||||
<SideNavHeader
|
||||
icon={<Calendar />}
|
||||
action={
|
||||
user ? (
|
||||
<Link to={EVENTS_PAGE} className={styles.viewAllLink}>
|
||||
{t("common:actions.viewAll")}
|
||||
<ChevronRight size={14} />
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{t("front:sideNav.myCalendar")}
|
||||
</SideNavHeader>
|
||||
{events.length > 0 ? (
|
||||
|
|
@ -309,6 +326,7 @@ export function Layout({
|
|||
<SideNavCollapseButton
|
||||
onToggle={() => setSideNavCollapsed(!sideNavCollapsed)}
|
||||
className={styles.sideNavCollapseButton}
|
||||
showNotificationDot={sideNavCollapsed && unseenIds.length > 0}
|
||||
/>
|
||||
<TopNavMenus />
|
||||
<TopRightButtons
|
||||
|
|
@ -337,7 +355,10 @@ function SiteTitle() {
|
|||
const hasBreadcrumbs = breadcrumbs.length > 0;
|
||||
|
||||
return (
|
||||
<Flipper flipKey={isFrontPage ? "front" : "other"}>
|
||||
<Flipper
|
||||
flipKey={isFrontPage ? "front" : "other"}
|
||||
className={styles.siteTitleFlipper}
|
||||
>
|
||||
<div className={styles.siteTitle}>
|
||||
<Flipped flipId="site-logo">
|
||||
<Link to="/" className={styles.siteLogo}>
|
||||
|
|
@ -376,19 +397,24 @@ function SiteLogoContent() {
|
|||
function SideNavCollapseButton({
|
||||
onToggle,
|
||||
className,
|
||||
showNotificationDot,
|
||||
}: {
|
||||
onToggle?: () => void;
|
||||
className?: string;
|
||||
showNotificationDot?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SendouButton
|
||||
className={className}
|
||||
variant="minimal"
|
||||
size="small"
|
||||
shape="square"
|
||||
icon={<PanelLeft />}
|
||||
onPress={onToggle}
|
||||
/>
|
||||
<div className={styles.sideNavCollapseButtonContainer}>
|
||||
<SendouButton
|
||||
className={className}
|
||||
variant="minimal"
|
||||
size="small"
|
||||
shape="square"
|
||||
icon={<PanelLeft />}
|
||||
onPress={onToggle}
|
||||
/>
|
||||
{showNotificationDot ? <NotificationDot /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -419,6 +445,7 @@ function MyRampUnit() {
|
|||
|
||||
function SideNavUserPanel() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const user = useUser();
|
||||
const { notifications, unseenIds } = useNotifications();
|
||||
|
||||
|
|
@ -433,11 +460,14 @@ function SideNavUserPanel() {
|
|||
</Link>
|
||||
<div className={sideNavStyles.sideNavFooterActions}>
|
||||
{notifications ? (
|
||||
<div className={sideNavStyles.sideNavFooterNotification}>
|
||||
<div
|
||||
className={sideNavStyles.sideNavFooterNotification}
|
||||
key={location.pathname}
|
||||
>
|
||||
{unseenIds.length > 0 ? (
|
||||
<div className={sideNavStyles.sideNavFooterUnseenDot}>
|
||||
<div className={sideNavStyles.sideNavFooterUnseenDotPulse} />
|
||||
</div>
|
||||
<NotificationDot
|
||||
className={sideNavStyles.sideNavFooterUnseenDot}
|
||||
/>
|
||||
) : null}
|
||||
<SendouPopover
|
||||
trigger={
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
|
|||
variation === "NO_SCRIMS" ? undefined : scrimPostRequests,
|
||||
associations,
|
||||
notifications,
|
||||
friendships,
|
||||
() => friendships(variation),
|
||||
liveStreams,
|
||||
];
|
||||
|
||||
|
|
@ -2800,7 +2800,7 @@ async function organization() {
|
|||
const SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS = [150, 151, 152, 153];
|
||||
const SENDOU_FRIEND_IDS_AS_TOURNAMENT_SUBS = [100, 101, 102, 103];
|
||||
|
||||
async function friendships() {
|
||||
async function friendships(variation?: SeedVariation | null) {
|
||||
const allFriendIds = [
|
||||
...SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS,
|
||||
...SENDOU_FRIEND_IDS_AS_TOURNAMENT_SUBS,
|
||||
|
|
@ -2820,6 +2820,8 @@ async function friendships() {
|
|||
.run({ userOneId, userTwoId });
|
||||
}
|
||||
|
||||
if (variation === "NO_SQ_GROUPS") return;
|
||||
|
||||
for (const friendId of SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS) {
|
||||
const group = await SQGroupRepository.createGroup({
|
||||
status: "ACTIVE",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import invariant from "~/utils/invariant";
|
|||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
ARTICLES_MAIN_PAGE,
|
||||
articlePage,
|
||||
articlePreviewUrl,
|
||||
navIconUrl,
|
||||
} from "~/utils/urls";
|
||||
|
|
@ -28,11 +27,6 @@ export const handle: SendouRouteHandle = {
|
|||
href: ARTICLES_MAIN_PAGE,
|
||||
type: "IMAGE",
|
||||
},
|
||||
{
|
||||
text: data.title,
|
||||
href: articlePage(data.slug),
|
||||
type: "TEXT",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
@ -64,12 +58,38 @@ export default function ArticlePage() {
|
|||
<div className="text-sm text-lighter">
|
||||
by <Author /> • <time>{data.dateString}</time>
|
||||
</div>
|
||||
<Markdown>{data.content}</Markdown>
|
||||
<Markdown>
|
||||
{contentWithoutLeadingTitle(data.content, data.title)}
|
||||
</Markdown>
|
||||
</article>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeText(text: string) {
|
||||
return text
|
||||
.replace(/\*+/g, "")
|
||||
.replace(/…/g, "...")
|
||||
.replace(/\\!/g, "!")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function contentWithoutLeadingTitle(content: string, title: string) {
|
||||
const trimmed = content.trimStart();
|
||||
const firstLineEnd = trimmed.indexOf("\n");
|
||||
const firstLine =
|
||||
firstLineEnd === -1 ? trimmed : trimmed.slice(0, firstLineEnd);
|
||||
|
||||
if (
|
||||
firstLine.startsWith("# ") &&
|
||||
normalizeText(firstLine.slice(2)) === normalizeText(title)
|
||||
) {
|
||||
return trimmed.slice(firstLine.length).trimStart();
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function Author() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export default function EditBadgePage() {
|
|||
<SendouDialog
|
||||
heading={`Editing winners of ${badge.displayName}`}
|
||||
onCloseTo={parentMatch.pathname}
|
||||
isFullScreen
|
||||
>
|
||||
<Form method="post" className="stack md">
|
||||
{isStaff ? <Managers data={data} /> : null}
|
||||
|
|
@ -53,13 +52,13 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
).length;
|
||||
|
||||
return (
|
||||
<div className="stack md mx-auto">
|
||||
<div className="stack md">
|
||||
<div className="stack sm">
|
||||
<h3 className={styles.editSmallHeader}>Managers</h3>
|
||||
<UserSearch
|
||||
key={managers.map((m) => m.id).join("-")}
|
||||
label="Add new manager"
|
||||
className="text-center mx-auto"
|
||||
className="text-center"
|
||||
name="new-manager"
|
||||
onChange={(user) => {
|
||||
if (!user) return;
|
||||
|
|
@ -134,12 +133,12 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
const userInputKey = owners.map((o) => `${o.id}-${o.count}`).join("-");
|
||||
|
||||
return (
|
||||
<div className="stack md mx-auto">
|
||||
<div className="stack md">
|
||||
<div className="stack sm">
|
||||
<h3 className={styles.editSmallHeader}>Owners</h3>
|
||||
<UserSearch
|
||||
label="Add new owner"
|
||||
className="text-center mx-auto"
|
||||
className="text-center"
|
||||
name="new-owner"
|
||||
key={userInputKey}
|
||||
onChange={(user) => {
|
||||
|
|
|
|||
|
|
@ -45,11 +45,6 @@ export const handle: SendouRouteHandle = {
|
|||
href: weaponBuildPage(data.meta.slug),
|
||||
type: "IMAGE",
|
||||
},
|
||||
{
|
||||
href: "/",
|
||||
text: data.meta.breadcrumbText,
|
||||
type: "TEXT",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,11 +48,6 @@ export const handle: SendouRouteHandle = {
|
|||
href: weaponBuildPage(data.meta.slug),
|
||||
type: "IMAGE",
|
||||
},
|
||||
{
|
||||
href: "/",
|
||||
text: data.meta.breadcrumbText,
|
||||
type: "TEXT",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
32
app/features/calendar/loaders/events.server.ts
Normal file
32
app/features/calendar/loaders/events.server.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
import {
|
||||
scrimToSidebarEvent,
|
||||
tournamentToSidebarEvent,
|
||||
} from "~/features/sidebar/core/sidebar.server";
|
||||
|
||||
export type EventsLoaderData = typeof loader;
|
||||
|
||||
export const loader = async () => {
|
||||
const user = requireUser();
|
||||
|
||||
const [tournamentsData, scrimsData] = await Promise.all([
|
||||
ShowcaseTournaments.frontPageTournamentsByUserId(user.id),
|
||||
ScrimPostRepository.findUserScrims(user.id),
|
||||
]);
|
||||
|
||||
const registered = tournamentsData.participatingFor
|
||||
.map(tournamentToSidebarEvent)
|
||||
.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
const hosting = tournamentsData.organizingFor
|
||||
.map(tournamentToSidebarEvent)
|
||||
.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
const scrims = scrimsData
|
||||
.map(scrimToSidebarEvent)
|
||||
.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
return { registered, hosting, scrims };
|
||||
};
|
||||
22
app/features/calendar/routes/events.module.css
Normal file
22
app/features/calendar/routes/events.module.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.eventsListHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-2);
|
||||
margin-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.filterRadio {
|
||||
padding: var(--s-1) var(--s-2);
|
||||
border-radius: var(--radius-field);
|
||||
cursor: pointer;
|
||||
color: var(--color-text-high);
|
||||
font-weight: var(--weight-semi);
|
||||
font-size: var(--font-3xs);
|
||||
}
|
||||
|
||||
.filterRadioSelected {
|
||||
background-color: var(--color-bg-higher);
|
||||
color: var(--color-text);
|
||||
}
|
||||
83
app/features/calendar/routes/events.tsx
Normal file
83
app/features/calendar/routes/events.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { Radio, RadioGroup } from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLoaderData } from "react-router";
|
||||
import { EventsList } from "~/components/EventsList";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { CALENDAR_PAGE } from "~/utils/urls";
|
||||
import type { EventsLoaderData } from "../loaders/events.server";
|
||||
import styles from "./events.module.css";
|
||||
|
||||
export { loader } from "../loaders/events.server";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["calendar"],
|
||||
};
|
||||
|
||||
type ViewFilter = "registered" | "hosting" | "scrims";
|
||||
|
||||
export default function EventsPage() {
|
||||
const { t } = useTranslation(["calendar"]);
|
||||
const data = useLoaderData<EventsLoaderData>();
|
||||
const [filter, setFilter] = useState<ViewFilter>("registered");
|
||||
|
||||
const viewLabels: Record<ViewFilter, string> = {
|
||||
registered: t("calendar:events.view.registered"),
|
||||
hosting: t("calendar:events.view.hosting"),
|
||||
scrims: t("calendar:events.view.scrims"),
|
||||
};
|
||||
|
||||
const shownEvents =
|
||||
filter === "registered"
|
||||
? data.registered
|
||||
: filter === "hosting"
|
||||
? data.hosting
|
||||
: data.scrims;
|
||||
|
||||
const hasNoEventsAtAll =
|
||||
data.registered.length === 0 &&
|
||||
data.hosting.length === 0 &&
|
||||
data.scrims.length === 0;
|
||||
|
||||
return (
|
||||
<Main halfWidth>
|
||||
<div className={styles.eventsListHeader}>
|
||||
<h2 className="text-lg mx-2">{t("calendar:events.title")}</h2>
|
||||
{hasNoEventsAtAll ? null : (
|
||||
<RadioGroup
|
||||
value={filter}
|
||||
onChange={(v) => setFilter(v as ViewFilter)}
|
||||
orientation="horizontal"
|
||||
className="stack horizontal xs"
|
||||
>
|
||||
{(["registered", "hosting", "scrims"] as const).map((value) => (
|
||||
<Radio key={value} value={value}>
|
||||
{({ isSelected }) => (
|
||||
<span
|
||||
className={clsx(styles.filterRadio, {
|
||||
[styles.filterRadioSelected]: isSelected,
|
||||
})}
|
||||
>
|
||||
{viewLabels[value]}
|
||||
</span>
|
||||
)}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
{hasNoEventsAtAll ? (
|
||||
<p className="text-lighter text-sm">
|
||||
{t("calendar:events.emptyAll")}{" "}
|
||||
<Link to={CALENDAR_PAGE}>{t("calendar:events.findOnCalendar")}</Link>
|
||||
</p>
|
||||
) : shownEvents.length === 0 ? (
|
||||
<p className="text-lighter text-sm">{t("calendar:events.empty")}</p>
|
||||
) : (
|
||||
<EventsList events={shownEvents} />
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
|
@ -44,10 +44,10 @@ export function getLiveTournamentStreams(): SidebarStream[] {
|
|||
return streams;
|
||||
}
|
||||
|
||||
// xxx: not always reporting furthest round
|
||||
function deriveCurrentRound(tournament: Tournament): string {
|
||||
for (const bracket of tournament.brackets) {
|
||||
for (const bracket of tournament.brackets.toReversed()) {
|
||||
if (bracket.preview) continue;
|
||||
if (bracket.isUnderground) continue;
|
||||
|
||||
for (const match of bracket.data.match) {
|
||||
const isActive =
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import * as FriendRepository from "~/features/friends/FriendRepository.server";
|
|||
import { resolveFriendActivity } from "~/features/friends/friends-utils.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
|
||||
import type { SidebarScrim } from "~/features/scrims/ScrimPostRepository.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
import { getSendouQSidebarStreams } from "~/features/sendouq-streams/core/streams.server";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
|
|
@ -81,27 +82,9 @@ export async function resolveSidebarData(userId: number | null) {
|
|||
seenTournamentIds.add(t.id);
|
||||
return true;
|
||||
})
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
url: t.url,
|
||||
logoUrl: t.logoUrl,
|
||||
startTime: t.startTime,
|
||||
type: "tournament" as const,
|
||||
}));
|
||||
.map(tournamentToSidebarEvent);
|
||||
|
||||
const scrimsIconUrl = `${navIconUrl("scrims")}.png`;
|
||||
const scrimEvents: SidebarEvent[] = scrimsData.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.opponentName ?? "Scrim",
|
||||
url: s.isAccepted
|
||||
? href("/scrims/:id", { id: String(s.id) })
|
||||
: href("/scrims"),
|
||||
logoUrl: s.opponentAvatarUrl ?? scrimsIconUrl,
|
||||
startTime: s.at,
|
||||
type: "scrim" as const,
|
||||
scrimStatus: s.isAccepted ? ("booked" as const) : ("looking" as const),
|
||||
}));
|
||||
const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);
|
||||
|
||||
const personalEvents = [...tournamentEvents, ...scrimEvents].sort(
|
||||
(a, b) => a.startTime - b.startTime,
|
||||
|
|
@ -347,3 +330,32 @@ function rowToSidebarFriend(
|
|||
tournamentId: row.tournamentId,
|
||||
};
|
||||
}
|
||||
|
||||
export function tournamentToSidebarEvent(
|
||||
t: ShowcaseCalendarEvent,
|
||||
): SidebarEvent {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
url: t.url,
|
||||
logoUrl: t.logoUrl,
|
||||
startTime: t.startTime,
|
||||
type: "tournament" as const,
|
||||
};
|
||||
}
|
||||
|
||||
const SCRIMS_ICON_URL = `${navIconUrl("scrims")}.png`;
|
||||
|
||||
export function scrimToSidebarEvent(s: SidebarScrim): SidebarEvent {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.opponentName ?? "Scrim",
|
||||
url: s.isAccepted
|
||||
? href("/scrims/:id", { id: String(s.id) })
|
||||
: href("/scrims"),
|
||||
logoUrl: s.opponentAvatarUrl ?? SCRIMS_ICON_URL,
|
||||
startTime: s.at,
|
||||
type: "scrim" as const,
|
||||
scrimStatus: s.isAccepted ? ("booked" as const) : ("looking" as const),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
parseRequestPayload,
|
||||
} from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { mySlugify, TEAM_SEARCH_PAGE, teamPage } from "~/utils/urls";
|
||||
import { mySlugify, teamPage } from "~/utils/urls";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
import { editTeamSchema, teamParamsSchema } from "../team-schemas.server";
|
||||
import {
|
||||
|
|
@ -60,7 +60,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
}
|
||||
case "DELETE_TEAM": {
|
||||
await TeamRepository.del(team.id);
|
||||
throw redirect(TEAM_SEARCH_PAGE);
|
||||
throw redirect("/");
|
||||
}
|
||||
case "DELETE_AVATAR": {
|
||||
await TeamRepository.removeTeamImage(team.id, "avatar");
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type Theme = (typeof Theme)[keyof typeof Theme];
|
|||
const themes = Object.values(Theme);
|
||||
|
||||
type ThemeContextType = {
|
||||
htmlThemeClass: Theme;
|
||||
htmlThemeClass: Theme | "";
|
||||
metaColorScheme: "light dark" | "dark light";
|
||||
userTheme: Theme | "auto" | null;
|
||||
setUserTheme: (newTheme: Theme | "auto") => void;
|
||||
|
|
@ -39,7 +39,7 @@ function useSystemTheme() {
|
|||
return useSyncExternalStore(
|
||||
subscribeToSystemTheme,
|
||||
getSystemTheme,
|
||||
() => Theme.DARK,
|
||||
() => null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ type ThemeProviderProps = {
|
|||
themeSource: "user-preference" | "static";
|
||||
};
|
||||
|
||||
function colorScheme(theme: Theme) {
|
||||
function colorScheme(theme: Theme | "") {
|
||||
return theme === Theme.LIGHT ? "light dark" : "dark light";
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ function ThemeProvider({
|
|||
const systemTheme = useSystemTheme();
|
||||
const persistThemeFetcher = useFetcher();
|
||||
|
||||
const resolvedTheme = isStatic
|
||||
const resolvedTheme: Theme | "" = isStatic
|
||||
? (specifiedTheme ?? Theme.DARK)
|
||||
: userPreference === "auto"
|
||||
? systemTheme
|
||||
? (systemTheme ?? "")
|
||||
: userPreference;
|
||||
|
||||
const handleSetUserTheme = (newTheme: Theme | "auto") => {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ function MatchRow({
|
|||
>
|
||||
<div
|
||||
className={clsx(styles.matchSeed, {
|
||||
"text-lighter-important italic opaque": simulated,
|
||||
"text-lighter italic opaque": simulated,
|
||||
[styles.matchSeedWide]: isBigSeedNumber,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ function RegistrationProgress({
|
|||
</h3>
|
||||
<section className={clsx(styles.section, "stack md")}>
|
||||
<div className="stack horizontal lg justify-center text-sm font-semi-bold">
|
||||
{steps.map((step) => {
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<div
|
||||
key={step.name}
|
||||
|
|
@ -449,6 +449,7 @@ function RegistrationProgress({
|
|||
{step.status === "completed" ? (
|
||||
<Check
|
||||
className={clsx(styles.sectionIcon, "color-success")}
|
||||
data-testid={`checkmark-icon-num-${i + 1}`}
|
||||
/>
|
||||
) : step.status === "notice" ? (
|
||||
<AlertCircle
|
||||
|
|
@ -1045,7 +1046,7 @@ function FillRoster({
|
|||
) : null}
|
||||
</section>
|
||||
{tournament.ctx.settings.requireInGameNames ? (
|
||||
<div className={clsx(styles.sectionWarning, "text-warning-important")}>
|
||||
<div className={clsx(styles.sectionWarning, "text-warning")}>
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { removeMarkdown } from "~/utils/strings";
|
|||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
tournamentDivisionsPage,
|
||||
tournamentOrganizationPage,
|
||||
tournamentPage,
|
||||
tournamentRegisterPage,
|
||||
} from "~/utils/urls";
|
||||
|
|
@ -70,24 +69,13 @@ export const handle: SendouRouteHandle = {
|
|||
const data = JSON.parse(rawData) as TournamentLoaderData;
|
||||
|
||||
return [
|
||||
data.tournament.ctx.organization?.logoUrl
|
||||
? {
|
||||
imgPath: data.tournament.ctx.organization.logoUrl,
|
||||
href: tournamentOrganizationPage({
|
||||
organizationSlug: data.tournament.ctx.organization.slug,
|
||||
}),
|
||||
type: "IMAGE" as const,
|
||||
text: "",
|
||||
rounded: true,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
imgPath: data.tournament.ctx.logoUrl,
|
||||
href: tournamentPage(data.tournament.ctx.id),
|
||||
type: "IMAGE" as const,
|
||||
text: data.tournament.ctx.name,
|
||||
},
|
||||
].filter((crumb) => crumb !== null);
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useHasRole } from "~/modules/permissions/hooks";
|
|||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
discordAvatarUrl,
|
||||
userAdminPage,
|
||||
userArtPage,
|
||||
userBuildsPage,
|
||||
|
|
@ -44,10 +45,23 @@ export const handle: SendouRouteHandle = {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
if (!data.user.discordAvatar) {
|
||||
return {
|
||||
text: data.user.username,
|
||||
href: userPage(data.user),
|
||||
type: "TEXT",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: data.user.username,
|
||||
imgPath: discordAvatarUrl({
|
||||
discordId: data.user.discordId,
|
||||
discordAvatar: data.user.discordAvatar,
|
||||
size: "sm",
|
||||
}),
|
||||
href: userPage(data.user),
|
||||
type: "TEXT",
|
||||
type: "IMAGE",
|
||||
text: data.user.username,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ function Document({
|
|||
dir={i18n.dir()}
|
||||
className={clsx(htmlThemeClass, "scrollbar")}
|
||||
style={Object.fromEntries(customThemeStyle)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export default [
|
|||
|
||||
route("/friends", "features/friends/routes/friends.tsx"),
|
||||
|
||||
route("/events", "features/calendar/routes/events.tsx"),
|
||||
|
||||
route("/suspended", "features/ban/routes/suspended.tsx"),
|
||||
|
||||
route("/u", "features/user-search/routes/u.tsx"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
1566
app/styles/flags.css
1566
app/styles/flags.css
File diff suppressed because it is too large
Load Diff
227
app/styles/normalize.css
vendored
227
app/styles/normalize.css
vendored
|
|
@ -1,214 +1,217 @@
|
|||
/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */
|
||||
@layer reset {
|
||||
/*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */
|
||||
|
||||
/*
|
||||
/*
|
||||
Document
|
||||
========
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
Use a better box model (opinionated).
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
||||
2. Correct the line height in all browsers.
|
||||
3. Prevent adjustments of font size after orientation changes in iOS.
|
||||
4. Use a more readable tab size (opinionated).
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family:
|
||||
system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji"; /* 1 */
|
||||
line-height: 1.15; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 3 */
|
||||
tab-size: 4; /* 4 */
|
||||
}
|
||||
html {
|
||||
font-family:
|
||||
system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji"; /* 1 */
|
||||
line-height: 1.15; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 3 */
|
||||
tab-size: 4; /* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Sections
|
||||
========
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Text-level semantics
|
||||
====================
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
Add the correct font weight in Chrome and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
||||
2. Correct the odd 'em' font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
|
||||
monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Tabular data
|
||||
============
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
*/
|
||||
|
||||
table {
|
||||
border-color: currentcolor;
|
||||
}
|
||||
table {
|
||||
border-color: currentcolor;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Forms
|
||||
=====
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
appearance: button;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
appearance: button;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
appearance: textfield; /* 1 */
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
[type="search"] {
|
||||
appearance: textfield; /* 1 */
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to 'inherit' in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Interactive
|
||||
===========
|
||||
*/
|
||||
|
||||
/*
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
@layer reset, base, components, utilities;
|
||||
|
||||
/*
|
||||
⚠️⚠️⚠️
|
||||
Make sure to read styles.md before editing colors in this file,
|
||||
Make sure to read styles.md before editing colors in this file,
|
||||
it contains important information about how the styles work and how to edit them properly
|
||||
⚠️⚠️⚠️
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export const SENDOUQ_STREAMS_PAGE = "/q/streams";
|
|||
export const TIERS_PAGE = "/tiers";
|
||||
export const SUSPENDED_PAGE = "/suspended";
|
||||
export const LFG_PAGE = "/lfg";
|
||||
export const EVENTS_PAGE = "/events";
|
||||
export const FRIENDS_PAGE = "/friends";
|
||||
export const SETTINGS_PAGE = "/settings";
|
||||
export const LUTI_PAGE = "/luti";
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -12,33 +12,9 @@ import {
|
|||
test,
|
||||
} from "~/utils/playwright";
|
||||
import { createFormHelpers } from "~/utils/playwright-form";
|
||||
import {
|
||||
editTeamPage,
|
||||
TEAM_SEARCH_PAGE,
|
||||
teamPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
|
||||
test.describe("Team search page", () => {
|
||||
test("filters teams", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({ page, url: TEAM_SEARCH_PAGE });
|
||||
|
||||
const searchInput = page.getByTestId("team-search-input");
|
||||
const firstTeamName = page.getByTestId("team-0");
|
||||
const secondTeamName = page.getByTestId("team-1");
|
||||
|
||||
await expect(firstTeamName).toHaveText("Alliance Rogue");
|
||||
await expect(secondTeamName).toBeVisible();
|
||||
|
||||
await searchInput.fill("Alliance Rogue");
|
||||
await expect(secondTeamName).not.toBeVisible();
|
||||
|
||||
await firstTeamName.click();
|
||||
await expect(page).toHaveURL(/alliance-rogue/);
|
||||
});
|
||||
import { editTeamPage, teamPage, userPage } from "~/utils/urls";
|
||||
|
||||
test.describe("New team creation", () => {
|
||||
test("creates new team", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
|
|
@ -47,7 +23,7 @@ test.describe("Team search page", () => {
|
|||
await page.getByTestId("anything-adder-menu-button").click();
|
||||
await page.getByTestId("menu-item-team").click();
|
||||
|
||||
await expect(page).toHaveURL(/new=true/);
|
||||
await expect(page).toHaveURL(/t\/new/);
|
||||
|
||||
const form = createFormHelpers(page, createTeamSchema);
|
||||
await form.fill("name", "Chimera");
|
||||
|
|
@ -55,25 +31,6 @@ test.describe("Team search page", () => {
|
|||
|
||||
await expect(page).toHaveURL(/chimera/);
|
||||
});
|
||||
|
||||
test("filters teams by tag & displays tag", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, ADMIN_ID);
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
await page.getByTestId("edit-team-button").click();
|
||||
await page.getByLabel("Tag").fill("AR");
|
||||
await submit(page, "edit-team-submit-button");
|
||||
|
||||
await navigate({ page, url: TEAM_SEARCH_PAGE });
|
||||
|
||||
const searchInput = page.getByTestId("team-search-input");
|
||||
await searchInput.fill("ar");
|
||||
|
||||
const firstTeamName = page.getByTestId("team-0");
|
||||
await expect(firstTeamName).toContainText("Alliance Rogue");
|
||||
await expect(firstTeamName).toContainText("AR");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Team page", () => {
|
||||
|
|
@ -129,16 +86,13 @@ test.describe("Team page", () => {
|
|||
await seed(page);
|
||||
await impersonate(page, ADMIN_ID);
|
||||
|
||||
await navigate({ page, url: TEAM_SEARCH_PAGE });
|
||||
const firstTeamName = page.getByTestId("team-0");
|
||||
await firstTeamName.click();
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
await page.getByTestId("edit-team-button").click();
|
||||
await page.getByTestId("delete-team-button").click();
|
||||
await modalClickConfirmButton(page);
|
||||
|
||||
await expect(page).toHaveURL(TEAM_SEARCH_PAGE);
|
||||
await expect(page.getByTestId("team-0")).not.toHaveText("Alliance Rogue");
|
||||
await expect(page).not.toHaveURL(/alliance-rogue/);
|
||||
});
|
||||
|
||||
test("resets invite code, joins team, leaves, rejoins", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ test.describe("Tournament streams", () => {
|
|||
await backToBracket(page);
|
||||
|
||||
// The LIVE button should be visible since team 102 members are streaming
|
||||
const liveButton = page.getByText("LIVE").first();
|
||||
const bracketsViewer = page.getByTestId("brackets-viewer");
|
||||
const liveButton = bracketsViewer.getByText("LIVE").first();
|
||||
await expect(liveButton).toBeVisible();
|
||||
|
||||
// Click the LIVE button to open the popover
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ import { userEditProfilePage, userPage } from "~/utils/urls";
|
|||
|
||||
const goToEditPage = (page: Page) =>
|
||||
page.getByText("Edit", { exact: true }).click();
|
||||
const submitEditForm = (page: Page) =>
|
||||
page.getByText("Save", { exact: true }).click();
|
||||
|
||||
test.describe("User page", () => {
|
||||
test("uses badge pagination", async ({ page }) => {
|
||||
|
|
@ -115,54 +113,38 @@ test.describe("User page", () => {
|
|||
await page.getByText("Stick 0 / Motion -5").isVisible();
|
||||
});
|
||||
|
||||
test("customizes user page colors and resets them", async ({ page }) => {
|
||||
test("customizes theme colors and resets them", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: userPage({ discordId: ADMIN_DISCORD_ID, customUrl: "sendou" }),
|
||||
});
|
||||
|
||||
const body = page.locator("body");
|
||||
const bodyColor = () =>
|
||||
body.evaluate((element) =>
|
||||
window.getComputedStyle(element).getPropertyValue("--bg").trim(),
|
||||
const htmlElement = page.locator("html");
|
||||
const hasCustomTheme = () =>
|
||||
htmlElement.evaluate(
|
||||
(el) => el.style.getPropertyValue("--_base-h") !== "",
|
||||
);
|
||||
|
||||
await expect(bodyColor()).resolves.toMatch(/#ebebf0/);
|
||||
await navigate({ page, url: "/settings" });
|
||||
|
||||
await goToEditPage(page);
|
||||
// initially no custom theme
|
||||
await expect(hasCustomTheme()).resolves.toBe(false);
|
||||
|
||||
await page.locator("span").filter({ hasText: "Custom colors" }).click();
|
||||
// change the base hue slider
|
||||
const baseHueSlider = page.locator("#base-hue");
|
||||
await baseHueSlider.fill("120");
|
||||
|
||||
await page.getByTestId("color-input-bg").fill("#4a412a");
|
||||
|
||||
// also test filling this because it's a special case as it also changes bg-lightest
|
||||
await page.getByTestId("color-input-bg-lighter").fill("#4a412a");
|
||||
|
||||
await submitEditForm(page);
|
||||
|
||||
// got redirected
|
||||
await expect(page).not.toHaveURL(/edit/);
|
||||
// save
|
||||
await page.getByRole("button", { name: "Save" }).first().click();
|
||||
await page.reload();
|
||||
await expect(bodyColor()).resolves.toMatch(/#4a412a/);
|
||||
|
||||
// then lets test resetting the colors is possible
|
||||
await goToEditPage(page);
|
||||
await page.locator("span").filter({ hasText: "Custom colors" }).click();
|
||||
// verify custom theme was applied
|
||||
await expect(hasCustomTheme()).resolves.toBe(true);
|
||||
|
||||
for (const button of await page
|
||||
.getByRole("button", { name: "Reset" })
|
||||
.all()) {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
await submitEditForm(page);
|
||||
|
||||
// got redirected
|
||||
await expect(page).not.toHaveURL(/edit/);
|
||||
// reset
|
||||
await page.getByRole("button", { name: "Reset" }).first().click();
|
||||
await page.reload();
|
||||
await expect(bodyColor()).resolves.toMatch(/#ebebf0/);
|
||||
|
||||
// verify custom theme was removed
|
||||
await expect(hasCustomTheme()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("edits weapon pool", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -79,5 +79,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,5 +79,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,5 +79,12 @@
|
|||
"filter.applyAndDefault": "Apply & make default",
|
||||
"forms.draft": "Draft",
|
||||
"forms.draftInfo": "Draft tournaments are hidden and only visible to organizers. The tournament must be opened (by disabling this toggle) before any bracket can be started.",
|
||||
"forms.draftBracketStartBlocked": "Tournament is in draft mode. Edit the tournament and disable the draft toggle before starting the bracket."
|
||||
"forms.draftBracketStartBlocked": "Tournament is in draft mode. Edit the tournament and disable the draft toggle before starting the bracket.",
|
||||
"events.title": "My Events",
|
||||
"events.view.registered": "Registered",
|
||||
"events.view.hosting": "Hosting",
|
||||
"events.view.scrims": "Scrims",
|
||||
"events.empty": "No events in this category",
|
||||
"events.emptyAll": "You have no upcoming events.",
|
||||
"events.findOnCalendar": "Find an event to join on the calendar!"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "החל והפוך לברירת מחדל",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,5 +75,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,5 +75,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,5 +79,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,5 +83,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,5 +83,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,5 +75,12 @@
|
|||
"filter.applyAndDefault": "",
|
||||
"forms.draft": "",
|
||||
"forms.draftInfo": "",
|
||||
"forms.draftBracketStartBlocked": ""
|
||||
"forms.draftBracketStartBlocked": "",
|
||||
"events.title": "",
|
||||
"events.view.registered": "",
|
||||
"events.view.hosting": "",
|
||||
"events.view.scrims": "",
|
||||
"events.empty": "",
|
||||
"events.emptyAll": "",
|
||||
"events.findOnCalendar": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,20 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
// Wraps CSS modules in @layer components so utility classes always win.
|
||||
// The layer order declaration is prepended to each module because in Vite
|
||||
// dev mode, module <style> tags are injected before global stylesheets —
|
||||
// without it the implicit first @layer components would get lowest priority.
|
||||
name: "css-modules-layer",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
if (!id.endsWith(".module.css")) return;
|
||||
return {
|
||||
code: `@layer reset, base, components, utilities;\n@layer components {\n${code}\n}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
reactRouter(),
|
||||
babel({
|
||||
filter: /\.[jt]sx?$/,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user