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

199 lines
5.5 KiB
TypeScript

import * as React from "react"
import clsx from "clsx"
import type { SearchParams } from "../types/SearchParams"
import BulletIcon from "../icons/Bullet"
import EnglishIcon from "../icons/English"
import SortIcon from "../icons/Sort"
import KnightIcon from "../icons/Knight"
import LightningIcon from "../icons/Lightning"
import PawnIcon from "../icons/Pawn"
import RabbitIcon from "../icons/Rabbit"
import RightArrowIcon from "../icons/RightArrow"
import RisingGraphIcon from "../icons/RisingGraph"
import TrophyIcon from "../icons/Trophy"
import { Button } from "./Button"
import { Mode } from "../types/Mode"
import { Site } from "../types/Site"
import { Title } from "../types/Title"
interface SortOption {
title: string
Icon: ({ ...props }: { [x: string]: any }) => React.JSX.Element
enable: (p: SearchParams) => SearchParams
isEnabled: (p: SearchParams) => boolean
}
const filters: SortOption[] = [
{
title: "On Lichess",
Icon: KnightIcon,
enable: (p) => {
p.sites.push(Site.LICHESS)
return p
},
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"),
},
{
title: "ELO 2000+",
Icon: RisingGraphIcon,
enable: (p) => {
p.rating[0] = Math.max(2000, p.rating[0])
return p
},
isEnabled: (p) => p.rating[0] >= 2000,
},
{
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),
},
{
title: "Titled Player",
Icon: TrophyIcon,
enable: (p) => {
p.titles = Object.keys(Title) as Title[]
return p
},
isEnabled: (p) => p.titles.length > 0,
},
]
enum Direction {
LEFT,
RIGHT,
}
interface SortScrollProps {
params: SearchParams
onModal: () => void
onSelect: (p: SearchParams) => void
}
export function SortScroll({ params, onModal, onSelect }: SortScrollProps) {
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 (
<div className="flex items-center gap-x-8">
<div className="relative flex flex-grow overflow-hidden">
<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", {
"fill-amber-500 text-amber-500": e.isEnabled(params),
})}
onClick={() => onSelect(e.enable({ ...params }))}
>
<e.Icon className="mx-auto h-6 w-6" />
<span className="text-xs">{e.title}</span>
</div>
))}
</div>
<div
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>
</div>
</div>
<Button className="flex gap-x-2 py-4" onClick={onModal}>
<SortIcon className="h-6 w-6 fill-white" />
<span className="font-display">Sort</span>
</Button>
</div>
)
}