Apply initial ordering to search results.
parent
e927eab560
commit
286b3dd31d
|
@ -12,8 +12,8 @@ import { SelectLanguage, SelectLanguageProps } from "./SelectLanguage"
|
|||
import { Slider } from "./Slider"
|
||||
|
||||
import {
|
||||
FIDE_RATING_MIN,
|
||||
FIDE_RATING_MAX,
|
||||
FIDE_RATING_MIN as RATING_MIN,
|
||||
FIDE_RATING_MAX as RATING_MAX,
|
||||
SearchParams,
|
||||
} from "../types/SearchParams"
|
||||
|
||||
|
@ -55,8 +55,18 @@ export function FilterModal({
|
|||
}: FilterModalProps) {
|
||||
const idPrefix = React.useId()
|
||||
|
||||
const { watch, reset, control, register, setValue, handleSubmit } =
|
||||
useForm<SearchParams>({ defaultValues })
|
||||
const {
|
||||
watch,
|
||||
reset,
|
||||
control,
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<SearchParams>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
// Default values are processed immediately despite the modal not being open
|
||||
// at the start. Furthermore, values are preserved after closing and
|
||||
|
@ -80,7 +90,9 @@ export function FilterModal({
|
|||
}
|
||||
|
||||
const registerRating = register("rating")
|
||||
const registerModes = register("modes")
|
||||
const registerModes = register("modes", {
|
||||
required: "Please select at least one mode.",
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -91,7 +103,11 @@ export function FilterModal({
|
|||
as: "form",
|
||||
title: "Filters",
|
||||
footer: (
|
||||
<Button className="float-right py-2" type="submit">
|
||||
<Button
|
||||
className="float-right py-2"
|
||||
type="submit"
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
>
|
||||
Search coaches
|
||||
</Button>
|
||||
),
|
||||
|
@ -119,7 +135,7 @@ export function FilterModal({
|
|||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor={`${idPrefix}-rating`}>FIDE Rating:</Label>
|
||||
<Label htmlFor={`${idPrefix}-rating`}>Rating:</Label>
|
||||
<p className="py-2 text-sm">
|
||||
Find coaches that have a rating within the specified range. Keep in
|
||||
mind, a higher rating does not necessarily mean a better coach{" "}
|
||||
|
@ -141,14 +157,11 @@ export function FilterModal({
|
|||
setValue("rating.1", newValue[1])
|
||||
}}
|
||||
step={10}
|
||||
min={FIDE_RATING_MIN}
|
||||
max={FIDE_RATING_MAX}
|
||||
marks={computeStepLabels(
|
||||
FIDE_RATING_MIN,
|
||||
FIDE_RATING_MAX,
|
||||
7,
|
||||
50
|
||||
).map((s) => ({ value: s, label: `${s}` }))}
|
||||
min={RATING_MIN}
|
||||
max={RATING_MAX}
|
||||
marks={computeStepLabels(RATING_MIN, RATING_MAX, 7, 50).map(
|
||||
(s) => ({ value: s, label: `${s}` })
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -169,12 +182,13 @@ export function FilterModal({
|
|||
</div>
|
||||
</Field>
|
||||
|
||||
<FieldSet className="text-sm text-neutral-600">
|
||||
<p className="py-2">
|
||||
<FieldSet error={errors?.modes?.message}>
|
||||
<Label htmlFor={`${idPrefix}-rating`}>Mode:</Label>
|
||||
<p className="py-2 text-sm">
|
||||
Prefer a specific game mode? We{"'"}ll prioritize coaches that
|
||||
specialize in the modes selected.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 pt-3">
|
||||
<div className="grid grid-cols-3 pt-2 text-sm">
|
||||
{(Object.keys(Mode) as Mode[]).map((m) => (
|
||||
<div key={m} className="col-span-1 flex items-center gap-x-2">
|
||||
<CheckBox value={m} {...registerModes} />
|
||||
|
|
|
@ -14,3 +14,18 @@ export const defaultSearchParams: SearchParams = {
|
|||
modes: [Mode.RAPID, Mode.BLITZ, Mode.BULLET],
|
||||
languages: [],
|
||||
}
|
||||
|
||||
export function toQueryParams(p: SearchParams) {
|
||||
const queryParams: { [key: string]: any } = {}
|
||||
|
||||
for (const mode of p.modes) {
|
||||
queryParams[`${mode.toLowerCase()}_gte`] = p.rating[0]
|
||||
queryParams[`${mode.toLowerCase()}_lte`] = p.rating[1]
|
||||
}
|
||||
|
||||
if (p.languages.length > 0) {
|
||||
queryParams["languages"] = p.languages.join(",")
|
||||
}
|
||||
|
||||
return queryParams
|
||||
}
|
||||
|
|
|
@ -3,27 +3,30 @@ import { useQuery, useInfiniteQuery } from "@tanstack/react-query"
|
|||
|
||||
import type { Coach } from "../types/Coach"
|
||||
import type { Language } from "../types/Language"
|
||||
import type { SearchParams } from "../types/SearchParams"
|
||||
import { type SearchParams, toQueryParams } from "../types/SearchParams"
|
||||
|
||||
export const useCoachesInfiniteQuery = (searchParams: SearchParams) => {
|
||||
const queryParams = toQueryParams(searchParams)
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["coaches", searchParams],
|
||||
queryKey: ["coaches", queryParams],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await axios.get<{ data: Coach[] }>("/api/coaches/", {
|
||||
params: {
|
||||
page_no: pageParam,
|
||||
page_size: 15,
|
||||
...queryParams,
|
||||
},
|
||||
})
|
||||
return response.data.data
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage, _pages, lastPageParam) => {
|
||||
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
||||
if (lastPage.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return lastPageParam + 1
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -7,24 +7,71 @@ defmodule BoardWise.Coaches do
|
|||
alias BoardWise.Repo
|
||||
|
||||
alias BoardWise.Coaches.Coach
|
||||
alias BoardWise.Coaches.QueryParams
|
||||
|
||||
@prefix "coach_scraper"
|
||||
|
||||
defmacrop rating_fragment(field, gte, lte) do
|
||||
quote do
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? IS NULL THEN 0
|
||||
WHEN ? IS NULL THEN 0
|
||||
WHEN ? >= ? AND ? <= ? THEN 5
|
||||
ELSE 0
|
||||
END
|
||||
""",
|
||||
type(unquote(gte), :integer),
|
||||
type(unquote(lte), :integer),
|
||||
unquote(field),
|
||||
type(unquote(gte), :integer),
|
||||
unquote(field),
|
||||
type(unquote(lte), :integer)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the list of coaches at the given page, based on page size.
|
||||
Return the list of coaches according to the specified params.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> page_coaches(1, 25)
|
||||
iex> list_coaches(%QueryParams{...})
|
||||
[%Coach{}, ...]
|
||||
|
||||
"""
|
||||
def page_coaches(page_no, page_size) do
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
def list_coaches(%QueryParams{
|
||||
:rapid_gte => rapid_gte,
|
||||
:rapid_lte => rapid_lte,
|
||||
:blitz_gte => blitz_gte,
|
||||
:blitz_lte => blitz_lte,
|
||||
:bullet_gte => bullet_gte,
|
||||
:bullet_lte => bullet_lte,
|
||||
:languages => languages,
|
||||
:page_no => page_no,
|
||||
:page_size => page_size
|
||||
}) do
|
||||
Coach
|
||||
|> select([c], c)
|
||||
|> select_merge(
|
||||
[c],
|
||||
%{
|
||||
score:
|
||||
fragment(
|
||||
"""
|
||||
? + ? + ?
|
||||
""",
|
||||
rating_fragment(c.rapid, ^rapid_gte, ^rapid_lte),
|
||||
rating_fragment(c.blitz, ^blitz_gte, ^blitz_lte),
|
||||
rating_fragment(c.bullet, ^bullet_gte, ^bullet_lte)
|
||||
)
|
||||
|> selected_as(:score)
|
||||
}
|
||||
)
|
||||
|> order_by(desc: selected_as(:score))
|
||||
|> limit(^page_size)
|
||||
|> offset(^offset)
|
||||
|> offset(^((page_no - 1) * page_size))
|
||||
|> Repo.all(prefix: @prefix)
|
||||
end
|
||||
|
||||
|
|
|
@ -15,14 +15,20 @@ defmodule BoardWise.Coaches.Coach do
|
|||
import Ecto.Changeset
|
||||
|
||||
schema "export" do
|
||||
# required fields
|
||||
field :site, :string
|
||||
field :username, :string
|
||||
|
||||
# optional fields
|
||||
field :name, :string
|
||||
field :image_url, :string
|
||||
field :languages, {:array, :string}
|
||||
field :blitz, :integer
|
||||
field :bullet, :integer
|
||||
field :rapid, :integer
|
||||
|
||||
# virtual fields
|
||||
field :score, :integer, virtual: true
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
defmodule BoardWise.Coaches.QueryParams do
|
||||
defstruct [
|
||||
:rapid_gte,
|
||||
:rapid_lte,
|
||||
:blitz_gte,
|
||||
:blitz_lte,
|
||||
:bullet_gte,
|
||||
:bullet_lte,
|
||||
:languages,
|
||||
page_no: 1,
|
||||
page_size: 15
|
||||
]
|
||||
end
|
|
@ -1,28 +1,42 @@
|
|||
defmodule BoardWiseWeb.CoachController do
|
||||
use BoardWiseWeb, :controller
|
||||
require Logger
|
||||
|
||||
alias BoardWise.Coaches
|
||||
|
||||
plug :fetch_query_params
|
||||
alias BoardWise.Coaches.QueryParams
|
||||
|
||||
def index(conn, params) do
|
||||
page_no = get_integer_param(params, "page_no", 1)
|
||||
page_size = get_integer_param(params, "page_size", 10)
|
||||
coaches = Coaches.page_coaches(page_no, page_size)
|
||||
query_params =
|
||||
%QueryParams{}
|
||||
|> override_param(:rapid_gte, params, :integer)
|
||||
|> override_param(:rapid_lte, params, :integer)
|
||||
|> override_param(:blitz_gte, params, :integer)
|
||||
|> override_param(:blitz_lte, params, :integer)
|
||||
|> override_param(:bullet_gte, params, :integer)
|
||||
|> override_param(:bullet_lte, params, :integer)
|
||||
|> override_param(:languages, params, :strlist)
|
||||
|> override_param(:page_no, params, :integer)
|
||||
|> override_param(:page_size, params, :integer)
|
||||
|
||||
# Ensure we never attempt to query too large of a response all at once.
|
||||
query_params = %{query_params | page_size: Enum.min([query_params.page_size, 25])}
|
||||
|
||||
coaches = Coaches.list_coaches(query_params)
|
||||
render(conn, :index, coaches: coaches)
|
||||
end
|
||||
|
||||
defp get_integer_param(params, key, default) do
|
||||
val = Map.get(params, key)
|
||||
defp override_param(query_params, key, params, type) do
|
||||
case Map.get(params, Atom.to_string(key)) do
|
||||
nil ->
|
||||
query_params
|
||||
|
||||
if is_nil(val) do
|
||||
default
|
||||
else
|
||||
case Integer.parse(val) do
|
||||
{parsed, ""} -> parsed
|
||||
_ -> default
|
||||
end
|
||||
val when type == :strlist ->
|
||||
%{query_params | key => String.split(val, ",")}
|
||||
|
||||
val when type == :integer ->
|
||||
case Integer.parse(val) do
|
||||
{parsed, ""} -> %{query_params | key => parsed}
|
||||
_ -> query_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue