diff --git a/src/modules/maps/listing/map-card.tsx b/src/modules/maps/listing/map-card.tsx index 15d967a..6d378e9 100644 --- a/src/modules/maps/listing/map-card.tsx +++ b/src/modules/maps/listing/map-card.tsx @@ -15,11 +15,15 @@ interface MapCardProps { map: MapControllerGetMapListingsDataItem | MapControllerGetMapByIdResponse; className?: string; expandLowest?: boolean; + starRange?: { + min: number; + max: number; + }; showChips?: boolean; compact?: boolean; } -export function MapCard({ map, className, expandLowest, showChips = true, compact = false }: MapCardProps) { +export function MapCard({ map, className, expandLowest, starRange, showChips = true, compact = false }: MapCardProps) { const t = useTranslations(); const linkSearch = usePersistedLeaderboardSearch(); const displayLeaderboards = getDisplayLeaderboards(map.leaderboards); @@ -47,7 +51,11 @@ export function MapCard({ map, className, expandLowest, showChips = true, compac linkSearch={linkSearch} className={className} compact={compact} - pills={showChips ? : undefined} + pills={ + showChips ? ( + + ) : undefined + } mobileMetadata={ <> {map.bsid && ( diff --git a/src/modules/maps/listing/map-filters.tsx b/src/modules/maps/listing/map-filters.tsx index fb9faee..ca73a5a 100644 --- a/src/modules/maps/listing/map-filters.tsx +++ b/src/modules/maps/listing/map-filters.tsx @@ -42,8 +42,8 @@ const SORT_OPTIONS: { value: MapControllerGetMapListingsSortBy }[] = [ { value: 'totalScores' } ]; -const DEFAULT_MIN_STARS = 0; -const DEFAULT_MAX_STARS = 16; +export const DEFAULT_MIN_STARS = 0; +export const DEFAULT_MAX_STARS = 16; // sorts that imply ranked-only results const RANKED_SORTS = new Set(['highestStars', 'latestRankedAt']); diff --git a/src/modules/maps/shared/map-difficulty-chip.tsx b/src/modules/maps/shared/map-difficulty-chip.tsx index 1ef2894..fd12125 100644 --- a/src/modules/maps/shared/map-difficulty-chip.tsx +++ b/src/modules/maps/shared/map-difficulty-chip.tsx @@ -20,10 +20,11 @@ interface MapDifficultyChipProps { mapId: number; leaderboard: MapCardLeaderboard; isExpanded: boolean; + isDimmed?: boolean; onExpandAction: () => void; } -export function MapDifficultyChip({ mapId, leaderboard, isExpanded, onExpandAction }: MapDifficultyChipProps) { +export function MapDifficultyChip({ mapId, leaderboard, isExpanded, isDimmed, onExpandAction }: MapDifficultyChipProps) { const [expandedWidth, setExpandedWidth] = useState(0); const linkSearch = usePersistedLeaderboardSearch(); const [measureElement, setMeasureElement] = useState(null); @@ -78,10 +79,11 @@ export function MapDifficultyChip({ mapId, leaderboard, isExpanded, onExpandActi }} className={cn( 'text-badge-foreground absolute inset-0 flex h-full w-full items-center overflow-hidden rounded-md shadow-sm ring-1 ring-black/10', - transitionsEnabled && 'transition-[box-shadow,padding] duration-200 ease-out', + transitionsEnabled && 'transition-[box-shadow,filter,opacity,padding] duration-200 ease-out', 'hover:shadow-md focus-visible:shadow-md focus-visible:outline-hidden', 'justify-center', isExpanded && 'px-1', + isDimmed && 'opacity-35 saturate-50 hover:opacity-90 focus-visible:opacity-100', getDifficultyBgClass(leaderboard.difficulty) )} > diff --git a/src/modules/maps/shared/map-difficulty-chips.tsx b/src/modules/maps/shared/map-difficulty-chips.tsx index dc8d26c..35a8715 100644 --- a/src/modules/maps/shared/map-difficulty-chips.tsx +++ b/src/modules/maps/shared/map-difficulty-chips.tsx @@ -1,25 +1,43 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { type MapCardLeaderboard, MapDifficultyChip } from '@/modules/maps/shared/map-difficulty-chip'; +export type StarRangeFilter = { + min: number; + max: number; +}; + interface MapDifficultyChipsProps { mapId: number; leaderboards: MapCardLeaderboard[]; expandLowest?: boolean; + starRange?: StarRangeFilter; } -export function MapDifficultyChips({ mapId, leaderboards, expandLowest }: MapDifficultyChipsProps) { - const defaultChipId = (expandLowest ? leaderboards.at(0) : leaderboards.at(-1))?.id ?? null; +export function MapDifficultyChips({ mapId, leaderboards, expandLowest, starRange }: MapDifficultyChipsProps) { + const matchingLeaderboards = starRange ? leaderboards.filter((leaderboard) => matchesStarRange(leaderboard, starRange)) : []; + const defaultLeaderboards = starRange ? matchingLeaderboards : leaderboards; + const defaultChipId = (expandLowest ? defaultLeaderboards.at(0) : defaultLeaderboards.at(-1))?.id ?? null; const [expandedChipId, setExpandedChipId] = useState(defaultChipId); + const starRangeKey = starRange ? `${starRange.min}-${starRange.max}` : ''; + const previousStarRangeKey = useRef(starRangeKey); useEffect(() => { setExpandedChipId((current) => { - if (current != null && leaderboards.some((lb) => lb.id === current)) return current; + if (previousStarRangeKey.current !== starRangeKey) { + previousStarRangeKey.current = starRangeKey; + return defaultChipId; + } + + if (current != null) { + const currentLeaderboard = leaderboards.find((leaderboard) => leaderboard.id === current); + if (currentLeaderboard && (!starRange || matchesStarRange(currentLeaderboard, starRange))) return current; + } return defaultChipId; }); - }, [defaultChipId, leaderboards]); + }, [defaultChipId, leaderboards, starRange, starRangeKey]); return (
@@ -29,9 +47,15 @@ export function MapDifficultyChips({ mapId, leaderboards, expandLowest }: MapDif mapId={mapId} leaderboard={lb} isExpanded={expandedChipId === lb.id} + isDimmed={!!starRange && !matchesStarRange(lb, starRange)} onExpandAction={() => setExpandedChipId(lb.id)} /> ))}
); } + +function matchesStarRange(leaderboard: MapCardLeaderboard, starRange: StarRangeFilter) { + const stars = leaderboard.realm.stars; + return leaderboard.realm.leaderboardStatus === 'RANKED' && stars >= starRange.min && stars <= starRange.max; +} diff --git a/src/routes/maps.tsx b/src/routes/maps.tsx index 3740ed2..1977bf4 100644 --- a/src/routes/maps.tsx +++ b/src/routes/maps.tsx @@ -3,7 +3,7 @@ import { createServerFn } from '@tanstack/react-start'; import { z } from 'zod'; import { MapCard } from '@/modules/maps/listing/map-card'; -import { MapFilters } from '@/modules/maps/listing/map-filters'; +import { DEFAULT_MAX_STARS, DEFAULT_MIN_STARS, MapFilters } from '@/modules/maps/listing/map-filters'; import { MAP_CONTROLLER_GET_MAP_LISTINGS_SORT_BY, MAP_CONTROLLER_GET_MAP_LISTINGS_SORT_DIRECTION, @@ -100,6 +100,15 @@ function MapsRoute() { const maps = response.data; const meta = response.metadata; const expandLowest = searchParams.sortBy === 'highestStars' && (searchParams.sortDirection ?? 'desc') === 'asc'; + const minStars = searchParams.minStars ?? DEFAULT_MIN_STARS; + const maxStars = searchParams.maxStars ?? DEFAULT_MAX_STARS; + const starRange = + minStars !== DEFAULT_MIN_STARS || maxStars !== DEFAULT_MAX_STARS + ? { + min: minStars, + max: maxStars + } + : undefined; const bgCandidates = maps.filter((m) => m.coverUrl).map((m) => m.coverUrl); const getPageHref = (page: number) => buildMapsHref(updateSearchParams(searchParams, { page: page > 1 ? page : undefined })); @@ -119,7 +128,7 @@ function MapsRoute() {
{maps.map((map) => ( - + ))}