Tournament cards tweaks
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled

Closes #2356
This commit is contained in:
Kalle
2026-08-13 19:05:02 +03:00
parent 8eb487c3cb
commit 0c2380d21d
6 changed files with 53 additions and 83 deletions

View File

@@ -10,6 +10,14 @@
margin-inline: auto;
}
.badgesCompact {
min-width: 0;
max-width: 100%;
min-height: 0;
flex-wrap: wrap;
justify-content: center;
}
.smallBadges {
display: grid;
place-items: center;

View File

@@ -17,6 +17,8 @@ export interface BadgeDisplayProps {
onChange?: (badgeIds: number[]) => void;
children?: React.ReactNode;
showText?: boolean;
/** Fit inside tight containers (e.g. a popover) instead of the fixed 20rem box */
compact?: boolean;
className?: string;
}
@@ -25,6 +27,7 @@ export function BadgeDisplay({
onChange,
children,
showText = true,
compact = false,
className,
}: BadgeDisplayProps) {
const { t } = useTranslation("badges");
@@ -69,6 +72,7 @@ export function BadgeDisplay({
) : null}
<div
className={clsx(className, styles.badges, {
[styles.badgesCompact]: compact,
"justify-center": smallBadges.length === 0,
})}
>

View File

@@ -32,6 +32,11 @@
color: var(--color-text-inverse);
}
.overflowTag {
background-color: var(--color-bg-higher);
color: var(--color-text-high);
}
:global(html.light) .tag {
color: var(--color-text);
}

View File

@@ -12,10 +12,13 @@ export function Tags({
onDelete,
small = false,
centered = false,
maxVisible,
}: {
tags: Array<CalendarEventTag>;
small?: boolean;
centered?: boolean;
/** How many tags to show at most, rest are collapsed into a "+N" indicator. If undefined all tags are shown. */
maxVisible?: number;
/** Called when tag delete button clicked. If undefined delete buttons won't be shown. */
onDelete?: (tag: CalendarEventTag) => void;
@@ -24,6 +27,9 @@ export function Tags({
if (tags.length === 0) return null;
const visibleTags = maxVisible ? tags.slice(0, maxVisible) : tags;
const hiddenCount = tags.length - visibleTags.length;
return (
<ul
className={clsx(styles.tags, {
@@ -31,7 +37,7 @@ export function Tags({
[styles.centered]: centered,
})}
>
{tags.map((tag) => (
{visibleTags.map((tag) => (
<React.Fragment key={tag}>
<li
style={{ backgroundColor: allTags[tag].color }}
@@ -51,6 +57,9 @@ export function Tags({
</li>
</React.Fragment>
))}
{hiddenCount > 0 ? (
<li className={styles.overflowTag}>+{hiddenCount}</li>
) : null}
</ul>
);
}

View File

@@ -30,6 +30,10 @@
}
}
.cardRanked {
box-shadow: inset 0 -3px 0 var(--color-accent);
}
.imgContainer {
background-color: var(--color-bg);
padding: var(--s-1-5);
@@ -105,19 +109,12 @@
font-weight: var(--weight-semi);
}
.modesPillContainer {
padding-inline-start: 12px;
}
.modesPill {
background-color: var(--color-bg-higher);
border-radius: var(--radius-selector);
width: max-content;
padding: 0 var(--s-1-5);
.modesRow {
display: flex;
flex: none;
gap: var(--s-1);
height: var(--selector-size);
align-items: center;
padding-inline-start: 12px;
}
.pillsContainer {
@@ -126,31 +123,6 @@
justify-content: end;
padding-inline-end: 12px;
width: 100%;
&.lonely {
padding-inline-end: 0px;
padding-inline-start: 12px;
}
}
.pill {
font-size: var(--font-2xs);
font-weight: var(--weight-bold);
border-radius: var(--radius-selector);
height: var(--selector-size);
padding: 0 var(--s-1-5);
display: grid;
place-items: center;
}
.pillRanked {
background-color: var(--color-info-low);
color: var(--color-text-accent);
& svg {
width: var(--tournament-card-icon-size);
height: var(--tournament-card-icon-size);
}
}
.firstPlacers {
@@ -172,11 +144,6 @@
min-width: 16px;
}
.badgeDisplay {
min-width: initial;
min-height: initial;
}
.vodIndicator {
font-size: var(--font-2xs);
font-weight: var(--weight-bold);
@@ -196,9 +163,7 @@
}
.badgePill {
background-color: var(--color-bg-higher);
border-radius: var(--radius-selector);
padding: 0 var(--s-1-5);
padding: 0 var(--s-1);
height: var(--selector-size);
}

View File

@@ -1,5 +1,5 @@
import clsx from "clsx";
import { ShieldMinus, Trophy as TrophyIcon, Users } from "lucide-react";
import { ShieldMinus, Users } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { SendouButton } from "~/components/elements/Button";
@@ -13,7 +13,6 @@ import { Trophy } from "~/features/trophies/components/Trophy";
import { useFormatDistanceToNow } from "~/hooks/intl/useFormatDistanceToNow";
import { useHydrated } from "~/hooks/useHydrated";
import { useSpoilerFree } from "~/hooks/useSpoilerFree";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import { databaseTimestampToDate } from "~/utils/dates";
import { navIconUrl } from "~/utils/urls";
import type { CalendarEvent, ShowcaseCalendarEvent } from "../calendar-types";
@@ -37,7 +36,7 @@ export function TournamentCard({
const isCalendar = tournament.type === "calendar";
const isHostedOnSendouInk = typeof tournament.isRanked === "boolean";
const modes =
tournament.modes && !isDefaultModes(tournament.modes)
isCalendar && tournament.modes && tournament.modes.length > 0
? tournament.modes
: null;
@@ -53,7 +52,12 @@ export function TournamentCard({
})}
data-testid="tournament-card"
>
<Link to={tournament.url} className={styles.card}>
<Link
to={tournament.url}
className={clsx(styles.card, {
[styles.cardRanked]: tournament.isRanked === true,
})}
>
<div className="stack horizontal justify-between">
{tournament.logoUrl ? (
<div className={styles.imgContainer}>
@@ -121,7 +125,7 @@ export function TournamentCard({
) : null}
{isCalendar ? (
<div className="stack sm items-center my-2">
<Tags tags={tournament.tags} small centered />
<Tags tags={tournament.tags} small centered maxVisible={2} />
</div>
) : null}
{isShowcase && tournament.firstPlacers.length > 0 ? (
@@ -140,17 +144,8 @@ export function TournamentCard({
{isShowcase && "hasVods" in tournament && tournament.hasVods ? (
<div className={styles.vodIndicator}>📺 VODs</div>
) : null}
{modes ? <ModesPill modes={modes} /> : null}
<div
className={clsx(styles.pillsContainer, {
[styles.lonely]: !modes && isHostedOnSendouInk,
})}
>
{tournament.isRanked ? (
<div className={clsx(styles.pill, styles.pillRanked)}>
<TrophyIcon />
</div>
) : null}
{modes ? <ModesRow modes={modes} /> : null}
<div className={styles.pillsContainer}>
{isCalendar &&
(tournament.trophy ||
(tournament.badges && tournament.badges.length > 0)) ? (
@@ -315,16 +310,12 @@ function ParticipantsPill({
);
}
function ModesPill({ modes }: { modes: NonNullable<CalendarEvent["modes"]> }) {
const size = 16;
function ModesRow({ modes }: { modes: NonNullable<CalendarEvent["modes"]> }) {
return (
<div className={styles.modesPillContainer}>
<div className={styles.modesPill}>
{modes.map((mode) => (
<ModeImage key={mode} mode={mode} size={size} />
))}
</div>
<div className={styles.modesRow}>
{modes.map((mode) => (
<ModeImage key={mode} mode={mode} size={18} />
))}
</div>
);
}
@@ -345,7 +336,7 @@ function PrizesPill({
className={styles.badgePill}
>
<Image
size={16}
size={18}
path={trophy ? navIconUrl("trophies") : navIconUrl("badges")}
alt="Badge prizes"
className={styles.badgeNavIcon}
@@ -356,20 +347,8 @@ function PrizesPill({
{trophy ? (
<Trophy model={trophy} className={styles.trophyPreview} />
) : badges ? (
<BadgeDisplay
badges={badges}
showText={false}
className={styles.badgeDisplay}
/>
<BadgeDisplay badges={badges} showText={false} compact />
) : null}
</SendouPopover>
);
}
/** Modes pill is only interesting when the tournament deviates from the standard ranked modes */
function isDefaultModes(modes: NonNullable<CalendarEvent["modes"]>) {
return (
modes.length === rankedModesShort.length &&
rankedModesShort.every((mode) => modes.includes(mode))
);
}