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