server/assets/js/react/components/SortScroll.tsx

199 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-12-04 13:42:32 +00:00
import * as React from "react"
import clsx from "clsx"
2023-12-04 20:35:01 +00:00
import type { SearchParams } from "../types/SearchParams"
2023-12-04 13:42:32 +00:00
2023-12-06 16:16:57 +00:00
import BulletIcon from "../icons/Bullet"
import EnglishIcon from "../icons/English"
2023-12-07 14:58:40 +00:00
import SortIcon from "../icons/Sort"
2023-12-07 14:44:59 +00:00
import KnightIcon from "../icons/Knight"
2023-12-06 16:16:57 +00:00
import LightningIcon from "../icons/Lightning"
2023-12-07 14:44:59 +00:00
import PawnIcon from "../icons/Pawn"
2023-12-06 16:16:57 +00:00
import RabbitIcon from "../icons/Rabbit"
2023-12-04 13:42:32 +00:00
import RightArrowIcon from "../icons/RightArrow"
import RisingGraphIcon from "../icons/RisingGraph"
2023-12-07 12:45:40 +00:00
import TrophyIcon from "../icons/Trophy"
2023-12-04 13:42:32 +00:00
import { Button } from "./Button"
2023-12-06 16:16:57 +00:00
import { Mode } from "../types/Mode"
2023-12-07 14:44:59 +00:00
import { Site } from "../types/Site"
2023-12-07 12:45:40 +00:00
import { Title } from "../types/Title"
2023-12-04 13:42:32 +00:00
2023-12-07 14:58:40 +00:00
interface SortOption {
2023-12-04 13:42:32 +00:00
title: string
Icon: ({ ...props }: { [x: string]: any }) => React.JSX.Element
2023-12-04 20:35:01 +00:00
enable: (p: SearchParams) => SearchParams
isEnabled: (p: SearchParams) => boolean
2023-12-04 13:42:32 +00:00
}
2023-12-07 14:58:40 +00:00
const filters: SortOption[] = [
2023-12-04 13:42:32 +00:00
{
2023-12-07 14:58:40 +00:00
title: "On Lichess",
Icon: KnightIcon,
enable: (p) => {
2023-12-07 14:58:40 +00:00
p.sites.push(Site.LICHESS)
return p
2023-12-04 13:42:32 +00:00
},
2023-12-07 14:58:40 +00:00
isEnabled: (p) => p.sites.includes(Site.LICHESS),
},
{
title: "On Chess.com",
Icon: PawnIcon,
enable: (p) => {
p.sites.push(Site.CHESSCOM)
return p
},
isEnabled: (p) => p.sites.includes(Site.CHESSCOM),
},
{
title: "English Speaking",
Icon: EnglishIcon,
enable: (p) => {
for (const lang of ["en-US", "en-GB"]) {
if (!p.languages.includes(lang)) {
p.languages.push(lang)
}
}
return p
},
// Using `||` doesn't match how `enable` works but this is probably closer
// to how people would expect the filter to operate.
isEnabled: (p) =>
p.languages.includes("en-US") || p.languages.includes("en-GB"),
2023-12-04 13:42:32 +00:00
},
2023-12-07 14:58:40 +00:00
{
title: "ELO 2000+",
Icon: RisingGraphIcon,
enable: (p) => {
p.rating[0] = Math.max(2000, p.rating[0])
return p
},
isEnabled: (p) => p.rating[0] >= 2000,
},
2023-12-06 16:16:57 +00:00
{
title: "Rapid Specialty",
Icon: RabbitIcon,
enable: (p) => {
p.modes = [Mode.RAPID]
return p
},
isEnabled: (p) => p.modes.length === 1 && p.modes.includes(Mode.RAPID),
},
{
title: "Blitz Specialty",
Icon: LightningIcon,
enable: (p) => {
p.modes = [Mode.BLITZ]
return p
},
isEnabled: (p) => p.modes.length === 1 && p.modes.includes(Mode.BLITZ),
},
{
title: "Bullet Specialty",
Icon: BulletIcon,
enable: (p) => {
p.modes = [Mode.BULLET]
return p
},
isEnabled: (p) => p.modes.length === 1 && p.modes.includes(Mode.BULLET),
},
2023-12-07 12:45:40 +00:00
{
title: "Titled Player",
Icon: TrophyIcon,
enable: (p) => {
2023-12-07 14:44:59 +00:00
p.titles = Object.keys(Title) as Title[]
2023-12-07 12:45:40 +00:00
return p
},
isEnabled: (p) => p.titles.length > 0,
},
2023-12-04 13:42:32 +00:00
]
enum Direction {
LEFT,
RIGHT,
}
2023-12-07 14:58:40 +00:00
interface SortScrollProps {
2023-12-04 20:35:01 +00:00
params: SearchParams
2023-12-04 13:42:32 +00:00
onModal: () => void
2023-12-04 20:35:01 +00:00
onSelect: (p: SearchParams) => void
2023-12-04 13:42:32 +00:00
}
2023-12-07 14:58:40 +00:00
export function SortScroll({ params, onModal, onSelect }: SortScrollProps) {
2023-12-04 13:42:32 +00:00
const viewport = React.useRef<HTMLDivElement>(null)
const [isFlush, setIsFlush] = React.useState([true, false])
const scrollDir = (dir: Direction) => {
const v = viewport.current
if (!v) {
return
}
const delta = v.clientWidth / 2
const left = v.scrollLeft + (dir == Direction.RIGHT ? delta : -delta)
v.scroll({ left, behavior: "smooth" })
const isFlushLeft = left <= 1
const isFlushRight = left + v.clientWidth >= v.scrollWidth
setIsFlush([isFlushLeft, isFlushRight])
}
return (
2023-12-06 16:16:57 +00:00
<div className="flex items-center gap-x-8">
<div className="relative flex flex-grow overflow-hidden">
2023-12-04 13:42:32 +00:00
<div
ref={viewport}
className="flex items-center gap-x-12 overflow-hidden"
>
{[...filters].map((e) => (
<div
key={e.title}
className={clsx("flex-none cursor-pointer text-center", {
2023-12-04 20:35:01 +00:00
"fill-amber-500 text-amber-500": e.isEnabled(params),
2023-12-04 13:42:32 +00:00
})}
2023-12-04 20:35:01 +00:00
onClick={() => onSelect(e.enable({ ...params }))}
2023-12-04 13:42:32 +00:00
>
<e.Icon className="mx-auto h-6 w-6" />
<span className="text-xs">{e.title}</span>
</div>
))}
</div>
2023-12-06 16:16:57 +00:00
<div
2023-12-04 13:42:32 +00:00
className={clsx(
"pointer-events-none absolute top-1/2 -translate-y-1/2 bg-gradient-to-r from-white to-transparent to-90% py-4 pr-60",
isFlush[0] ? "hidden" : ""
)}
>
<Button
className="pointer-events-auto rounded-full border border-neutral-900 py-4"
onClick={() => {
scrollDir(Direction.LEFT)
}}
invert
>
<RightArrowIcon className="h-3 w-3 rotate-180 fill-white" />
</Button>
</div>
<div
className={clsx(
"pointer-events-none absolute right-0 top-1/2 -translate-y-1/2 bg-gradient-to-r from-transparent from-10% to-white py-4 pl-60",
isFlush[1] ? "hidden" : ""
)}
>
<Button
className="pointer-events-auto rounded-full border border-neutral-900 py-4"
onClick={() => {
scrollDir(Direction.RIGHT)
}}
invert
>
<RightArrowIcon className="h-3 w-3 fill-white" />
</Button>
2023-12-06 16:16:57 +00:00
</div>
2023-12-04 13:42:32 +00:00
</div>
<Button className="flex gap-x-2 py-4" onClick={onModal}>
2023-12-07 14:58:40 +00:00
<SortIcon className="h-6 w-6 fill-white" />
<span className="font-display">Sort</span>
2023-12-04 13:42:32 +00:00
</Button>
</div>
)
}