mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-06 19:27:58 -05:00
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user