diff --git a/assets/js/react/components/FilterModal.tsx b/assets/js/react/components/FilterModal.tsx index 980f3b3..f9e7bbd 100644 --- a/assets/js/react/components/FilterModal.tsx +++ b/assets/js/react/components/FilterModal.tsx @@ -6,6 +6,7 @@ import { Field } from "./FieldSet" import { Input } from "./Input" import { Label } from "./Label" import { Modal } from "./Modal" +// import { SelectLanguage, SelectLanguageProps } from './SelectLanguage' import { Slider } from "./Slider" import { @@ -63,6 +64,19 @@ export function FilterModal({ // Registration + // const proxyLanguages = register('languages') + // const registerLanguages: Pick< + // SelectLanguageProps, + // 'defaultValue' | 'onChange' + // > = { + // ...proxyLanguages, + // defaultValue: defaultValues.languages, + // onChange: (event, value) => { + // event && proxyLanguages.onChange(event) + // setValue('languages', (value ?? []) as string[]) + // }, + // } + const controlFIDERating = register("fideRating") return ( @@ -82,6 +96,25 @@ export function FilterModal({ }} >
+ {/* + +

+ Select languages you prefer communicating in. We{"'"}ll prioritize + finding coaches that can speak fluently in at least one of your + selections. +

+ +
*/} +

diff --git a/assets/js/react/components/FilterScroll.tsx b/assets/js/react/components/FilterScroll.tsx index 57c43f2..da460d5 100644 --- a/assets/js/react/components/FilterScroll.tsx +++ b/assets/js/react/components/FilterScroll.tsx @@ -4,6 +4,7 @@ import clsx from "clsx" import type { SearchParams } from "../types/SearchParams" import FilterIcon from "../icons/Filter" +import EnglishIcon from "../icons/English" import RightArrowIcon from "../icons/RightArrow" import RisingGraphIcon from "../icons/RisingGraph" import { Button } from "./Button" @@ -19,11 +20,27 @@ const filters: FilterOption[] = [ { title: "FIDE 2000+", Icon: RisingGraphIcon, - enable: (q) => { - q.fideRating[0] = Math.max(2000, q.fideRating[0]) - return q + enable: (p) => { + p.fideRating[0] = Math.max(2000, p.fideRating[0]) + return p }, - isEnabled: (q) => q.fideRating[0] >= 2000, + isEnabled: (p) => p.fideRating[0] >= 2000, + }, + { + 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"), }, ] diff --git a/assets/js/react/components/Select.tsx b/assets/js/react/components/Select.tsx new file mode 100644 index 0000000..7fe57b1 --- /dev/null +++ b/assets/js/react/components/Select.tsx @@ -0,0 +1,105 @@ +import * as React from "react" +import clsx from "clsx" + +import { + Select as BaseSelect, + SelectProps, + SelectOwnerState, +} from "@mui/base/Select" +import { + Option as BaseOption, + OptionProps, + OptionOwnerState, +} from "@mui/base/Option" + +import { FieldContext } from "./FieldSet" +import { resolveSlotProps } from "../utils/props" +import { sameWidth } from "../utils/popperjs" + +export const Option = React.forwardRef>( + function Option(props, ref) { + const rootSlotProps = (ownerState: OptionOwnerState) => { + const resolved = resolveSlotProps(props.slotProps?.root, ownerState) + return { + ...resolved, + className: clsx( + "list-none p-2 rounded-lg cursor-default last-of-type:border-b-0", + ownerState.disabled + ? "text-slate-400" + : "hover:bg-slate-100 hover:text-slate-900", + { + "font-bold": ownerState.selected, + "bg-slate-100 text-slate-900": + ownerState.selected || ownerState.highlighted, + }, + resolved?.className + ), + } + } + + return ( + + ) + } +) + +export const Select = React.forwardRef(function Select< + TValue extends {}, + Multiple extends boolean, +>( + props: SelectProps, + ref: React.ForwardedRef +) { + const fieldContext = React.useContext(FieldContext) + const { disabled = fieldContext?.disabled, slotProps, ...other } = props + + const rootSlotProps = (ownerState: SelectOwnerState) => { + const resolved = resolveSlotProps(slotProps?.root, ownerState) + return { + ...resolved, + className: clsx( + "text-sm box-border px-3 py-2 rounded-lg text-left bg-white border border-solid text-slate-900 transition-all hover:bg-slate-50 outline-0 shadow shadow-slate-100 after:float-right", + ownerState.open ? 'after:content-["▴"]' : 'after:content-["▾"]', + ownerState.disabled ? "opacity-60" : "", + fieldContext?.error + ? "border-amber-800 focus-visible:outline focus-visible:outline-1 focus-visible:outline-amber-800" + : "border-slate-300", + resolved?.className + ), + } + } + + const listboxSlotProps = (ownerState: SelectOwnerState) => { + const resolved = resolveSlotProps(slotProps?.listbox, ownerState) + return { + ...resolved, + className: clsx( + "text-sm p-1.5 my-3 rounded-xl h-60 overflow-auto outline-0 bg-white border border-solid border-slate-200 text-slate-900 shadow shadow-slate-100", + resolved?.className + ), + } + } + + const popperSlotProps = (ownerState: SelectOwnerState) => { + const resolved = resolveSlotProps(slotProps?.popper, ownerState) + return { + ...resolved, + className: clsx("z-[1000]", resolved?.className), + modifiers: [sameWidth], + } + } + + return ( + + ) +}) diff --git a/assets/js/react/icons/English.tsx b/assets/js/react/icons/English.tsx new file mode 100644 index 0000000..1504562 --- /dev/null +++ b/assets/js/react/icons/English.tsx @@ -0,0 +1,30 @@ +import * as React from "react" + +const SvgComponent = ({ ...props }) => ( + + + + +) + +export default SvgComponent diff --git a/assets/js/react/types/Coach.ts b/assets/js/react/types/Coach.ts index 1e18501..8bd72bb 100644 --- a/assets/js/react/types/Coach.ts +++ b/assets/js/react/types/Coach.ts @@ -3,6 +3,7 @@ export type Coach = { username: string name: string | null image_url: string | null + languages: string[] | null rapid: number | null blitz: number | null bullet: number | null diff --git a/assets/js/react/types/Language.ts b/assets/js/react/types/Language.ts new file mode 100644 index 0000000..eefc960 --- /dev/null +++ b/assets/js/react/types/Language.ts @@ -0,0 +1,4 @@ +export type Language = { + name: string + code: string +} diff --git a/assets/js/react/types/SearchParams.ts b/assets/js/react/types/SearchParams.ts index 847d99d..743a657 100644 --- a/assets/js/react/types/SearchParams.ts +++ b/assets/js/react/types/SearchParams.ts @@ -1,5 +1,6 @@ export type SearchParams = { fideRating: [number, number] + languages: string[] } export const FIDE_RATING_MIN = 1500 @@ -7,4 +8,5 @@ export const FIDE_RATING_MAX = 3200 export const defaultSearchParams: SearchParams = { fideRating: [FIDE_RATING_MIN, FIDE_RATING_MAX], + languages: [], } diff --git a/assets/js/react/utils/popperjs.ts b/assets/js/react/utils/popperjs.ts new file mode 100644 index 0000000..50f66b8 --- /dev/null +++ b/assets/js/react/utils/popperjs.ts @@ -0,0 +1,16 @@ +import { type Modifier } from "@popperjs/core" + +export const sameWidth: Partial> = { + name: "sameWidth", + phase: "beforeWrite", + enabled: true, + requires: ["computeStyles"], + fn: ({ state }) => { + state.styles.popper.width = `${state.rects.reference.width}px` + }, + effect: ({ state }) => { + if ("offsetWidth" in state.elements.reference) { + state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px` + } + }, +} diff --git a/assets/package-lock.json b/assets/package-lock.json index f56f65a..132afb8 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@mui/base": "^5.0.0-beta.25", + "@popperjs/core": "^2.11.8", "@tanstack/react-query": "^5.12.2", "axios": "^1.6.2", "clsx": "^2.0.0", diff --git a/assets/package.json b/assets/package.json index ca357ae..dd74f3c 100644 --- a/assets/package.json +++ b/assets/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "dependencies": { "@mui/base": "^5.0.0-beta.25", + "@popperjs/core": "^2.11.8", "@tanstack/react-query": "^5.12.2", "axios": "^1.6.2", "clsx": "^2.0.0", diff --git a/lib/boardwise/coaches/coach.ex b/lib/boardwise/coaches/coach.ex index 43f5c7c..a61be17 100644 --- a/lib/boardwise/coaches/coach.ex +++ b/lib/boardwise/coaches/coach.ex @@ -19,6 +19,7 @@ defmodule BoardWise.Coaches.Coach do field :username, :string field :name, :string field :image_url, :string + field :languages, {:array, :string} field :blitz, :integer field :bullet, :integer field :rapid, :integer diff --git a/lib/boardwise_web/controllers/coach_json.ex b/lib/boardwise_web/controllers/coach_json.ex index 513f636..a1c8d97 100644 --- a/lib/boardwise_web/controllers/coach_json.ex +++ b/lib/boardwise_web/controllers/coach_json.ex @@ -14,6 +14,7 @@ defmodule BoardWiseWeb.CoachJSON do username: coach.username, name: coach.name, image_url: coach.image_url, + languages: coach.languages, rapid: coach.rapid, blitz: coach.blitz, bullet: coach.bullet diff --git a/priv/repo/migrations/20231205212321_languages.exs b/priv/repo/migrations/20231205212321_languages.exs new file mode 100644 index 0000000..4408bbf --- /dev/null +++ b/priv/repo/migrations/20231205212321_languages.exs @@ -0,0 +1,11 @@ +defmodule BoardWise.Repo.Migrations.Languages do + use Ecto.Migration + + @prefix "coach_scraper" + + def change do + alter table(:export, prefix: @prefix) do + add :languages, {:array, :string} + end + end +end