Migrate language selection. (#3)
* Setup to load in languages. * Retrieve languages from JSON endpoint. * Allow querying and filtering on language.main
parent
0487935038
commit
a83d54f6a2
|
@ -6,6 +6,7 @@ import { Field } from "./FieldSet"
|
||||||
import { Input } from "./Input"
|
import { Input } from "./Input"
|
||||||
import { Label } from "./Label"
|
import { Label } from "./Label"
|
||||||
import { Modal } from "./Modal"
|
import { Modal } from "./Modal"
|
||||||
|
import { SelectLanguage, SelectLanguageProps } from "./SelectLanguage"
|
||||||
import { Slider } from "./Slider"
|
import { Slider } from "./Slider"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -63,6 +64,19 @@ export function FilterModal({
|
||||||
|
|
||||||
// Registration
|
// 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")
|
const controlFIDERating = register("fideRating")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -82,6 +96,25 @@ export function FilterModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-12">
|
<div className="flex flex-col gap-12">
|
||||||
|
<Field>
|
||||||
|
<Label htmlFor={`${idPrefix}-languages`}>
|
||||||
|
Preferred Language(s):
|
||||||
|
</Label>
|
||||||
|
<p className="py-2 text-sm">
|
||||||
|
Select languages you prefer communicating in. We{"'"}ll prioritize
|
||||||
|
finding coaches that can speak fluently in at least one of your
|
||||||
|
selections.
|
||||||
|
</p>
|
||||||
|
<SelectLanguage
|
||||||
|
id={`${idPrefix}-languages`}
|
||||||
|
slotProps={{
|
||||||
|
root: { className: "w-full" },
|
||||||
|
}}
|
||||||
|
{...registerLanguages}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Label htmlFor={`${idPrefix}-fideRating`}>FIDE Rating:</Label>
|
<Label htmlFor={`${idPrefix}-fideRating`}>FIDE Rating:</Label>
|
||||||
<p className="py-2 text-sm">
|
<p className="py-2 text-sm">
|
||||||
|
|
|
@ -4,6 +4,7 @@ import clsx from "clsx"
|
||||||
import type { SearchParams } from "../types/SearchParams"
|
import type { SearchParams } from "../types/SearchParams"
|
||||||
|
|
||||||
import FilterIcon from "../icons/Filter"
|
import FilterIcon from "../icons/Filter"
|
||||||
|
import EnglishIcon from "../icons/English"
|
||||||
import RightArrowIcon from "../icons/RightArrow"
|
import RightArrowIcon from "../icons/RightArrow"
|
||||||
import RisingGraphIcon from "../icons/RisingGraph"
|
import RisingGraphIcon from "../icons/RisingGraph"
|
||||||
import { Button } from "./Button"
|
import { Button } from "./Button"
|
||||||
|
@ -19,11 +20,27 @@ const filters: FilterOption[] = [
|
||||||
{
|
{
|
||||||
title: "FIDE 2000+",
|
title: "FIDE 2000+",
|
||||||
Icon: RisingGraphIcon,
|
Icon: RisingGraphIcon,
|
||||||
enable: (q) => {
|
enable: (p) => {
|
||||||
q.fideRating[0] = Math.max(2000, q.fideRating[0])
|
p.fideRating[0] = Math.max(2000, p.fideRating[0])
|
||||||
return q
|
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"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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<HTMLLIElement, OptionProps<string>>(
|
||||||
|
function Option(props, ref) {
|
||||||
|
const rootSlotProps = (ownerState: OptionOwnerState<string>) => {
|
||||||
|
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 (
|
||||||
|
<BaseOption ref={ref} {...props} slotProps={{ root: rootSlotProps }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Select = React.forwardRef(function Select<
|
||||||
|
TValue extends {},
|
||||||
|
Multiple extends boolean,
|
||||||
|
>(
|
||||||
|
props: SelectProps<TValue, Multiple>,
|
||||||
|
ref: React.ForwardedRef<HTMLButtonElement>
|
||||||
|
) {
|
||||||
|
const fieldContext = React.useContext(FieldContext)
|
||||||
|
const { disabled = fieldContext?.disabled, slotProps, ...other } = props
|
||||||
|
|
||||||
|
const rootSlotProps = (ownerState: SelectOwnerState<TValue, Multiple>) => {
|
||||||
|
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<TValue, Multiple>) => {
|
||||||
|
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<TValue, Multiple>) => {
|
||||||
|
const resolved = resolveSlotProps(slotProps?.popper, ownerState)
|
||||||
|
return {
|
||||||
|
...resolved,
|
||||||
|
className: clsx("z-[1000]", resolved?.className),
|
||||||
|
modifiers: [sameWidth],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSelect
|
||||||
|
ref={ref}
|
||||||
|
{...other}
|
||||||
|
slotProps={{
|
||||||
|
...slotProps,
|
||||||
|
root: rootSlotProps,
|
||||||
|
listbox: listboxSlotProps,
|
||||||
|
popper: popperSlotProps,
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { SelectProps } from "@mui/base/Select"
|
||||||
|
|
||||||
|
import { Select, Option } from "./Select"
|
||||||
|
import { useFetchLanguages } from "../utils/queries"
|
||||||
|
|
||||||
|
export type SelectLanguageProps = SelectProps<{}, boolean>
|
||||||
|
|
||||||
|
export const SelectLanguage = React.forwardRef(function SelectLanguage(
|
||||||
|
props: SelectLanguageProps,
|
||||||
|
ref: React.ForwardedRef<HTMLButtonElement>
|
||||||
|
) {
|
||||||
|
const id = React.useId()
|
||||||
|
const [options, setOptions] = React.useState([
|
||||||
|
{ value: "", label: "Loading..." },
|
||||||
|
])
|
||||||
|
const { defaultValue, ...other } = props
|
||||||
|
const { isLoading, data } = useFetchLanguages()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOptions(data.map((row) => ({ value: row.code, label: row.name })))
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
ref={ref}
|
||||||
|
key={isLoading ? `${id}-loading` : `${id}-loaded`}
|
||||||
|
className={isLoading ? "text-slate-900/60" : ""}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{options.map((entry, index) => {
|
||||||
|
return (
|
||||||
|
<Option key={index} value={entry.value}>
|
||||||
|
{entry.label}
|
||||||
|
</Option>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,30 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const SvgComponent = ({ ...props }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={4}
|
||||||
|
d="M13 31V17h8M13 24h7.5M13 31h7.5M26 31V19M26 31v-6.5a4.5 4.5 0 0 1 4.5-4.5v0a4.5 4.5 0 0 1 4.5 4.5V31"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
x={6}
|
||||||
|
y={6}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={4}
|
||||||
|
rx={3}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SvgComponent
|
|
@ -1,8 +1,4 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import axios from "axios"
|
|
||||||
import { useQuery } from "@tanstack/react-query"
|
|
||||||
|
|
||||||
import type { Coach } from "../types/Coach"
|
|
||||||
|
|
||||||
import { Container } from "../components/Container"
|
import { Container } from "../components/Container"
|
||||||
import { FadeIn, FadeInStagger } from "../components/FadeIn"
|
import { FadeIn, FadeInStagger } from "../components/FadeIn"
|
||||||
|
@ -12,15 +8,10 @@ import { FilterScroll } from "../components/FilterScroll"
|
||||||
import { Loading } from "../components/Loading"
|
import { Loading } from "../components/Loading"
|
||||||
import { SearchResult } from "../components/SearchResult"
|
import { SearchResult } from "../components/SearchResult"
|
||||||
import { defaultSearchParams } from "../types/SearchParams"
|
import { defaultSearchParams } from "../types/SearchParams"
|
||||||
|
import { useFetchCoaches } from "../utils/queries"
|
||||||
|
|
||||||
function SearchResults() {
|
function SearchResults() {
|
||||||
const { isLoading, isError, data } = useQuery({
|
const { isLoading, isError, data } = useFetchCoaches()
|
||||||
queryKey: ["coaches"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await axios.get<{ data: Coach[] }>("/api/coaches/")
|
|
||||||
return response.data.data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading className="mt-40" loading />
|
return <Loading className="mt-40" loading />
|
||||||
|
|
|
@ -3,6 +3,7 @@ export type Coach = {
|
||||||
username: string
|
username: string
|
||||||
name: string | null
|
name: string | null
|
||||||
image_url: string | null
|
image_url: string | null
|
||||||
|
languages: string[] | null
|
||||||
rapid: number | null
|
rapid: number | null
|
||||||
blitz: number | null
|
blitz: number | null
|
||||||
bullet: number | null
|
bullet: number | null
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type Language = {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
export type SearchParams = {
|
export type SearchParams = {
|
||||||
fideRating: [number, number]
|
fideRating: [number, number]
|
||||||
|
languages: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FIDE_RATING_MIN = 1500
|
export const FIDE_RATING_MIN = 1500
|
||||||
|
@ -7,4 +8,5 @@ export const FIDE_RATING_MAX = 3200
|
||||||
|
|
||||||
export const defaultSearchParams: SearchParams = {
|
export const defaultSearchParams: SearchParams = {
|
||||||
fideRating: [FIDE_RATING_MIN, FIDE_RATING_MAX],
|
fideRating: [FIDE_RATING_MIN, FIDE_RATING_MAX],
|
||||||
|
languages: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { type Modifier } from "@popperjs/core"
|
||||||
|
|
||||||
|
export const sameWidth: Partial<Modifier<any, any>> = {
|
||||||
|
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`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import axios from "axios"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
import type { Coach } from "../types/Coach"
|
||||||
|
import type { Language } from "../types/Language"
|
||||||
|
|
||||||
|
export const useFetchCoaches = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["api", "coaches"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get<{ data: Coach[] }>("/api/coaches/")
|
||||||
|
return response.data.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFetchLanguages = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["api", "languages"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get<{ data: Language[] }>("/api/languages/")
|
||||||
|
return response.data.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mui/base": "^5.0.0-beta.25",
|
"@mui/base": "^5.0.0-beta.25",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tanstack/react-query": "^5.12.2",
|
"@tanstack/react-query": "^5.12.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mui/base": "^5.0.0-beta.25",
|
"@mui/base": "^5.0.0-beta.25",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tanstack/react-query": "^5.12.2",
|
"@tanstack/react-query": "^5.12.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
|
|
@ -22,7 +22,6 @@ defmodule BoardWise.Coaches do
|
||||||
def list_coaches do
|
def list_coaches do
|
||||||
Coach
|
Coach
|
||||||
|> limit(6)
|
|> limit(6)
|
||||||
|> where(site: "lichess")
|
|
||||||
|> Repo.all(prefix: @prefix)
|
|> Repo.all(prefix: @prefix)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule BoardWise.Coaches.Coach do
|
||||||
field :username, :string
|
field :username, :string
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :image_url, :string
|
field :image_url, :string
|
||||||
|
field :languages, {:array, :string}
|
||||||
field :blitz, :integer
|
field :blitz, :integer
|
||||||
field :bullet, :integer
|
field :bullet, :integer
|
||||||
field :rapid, :integer
|
field :rapid, :integer
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
defmodule BoardWise.Languages do
|
||||||
|
@moduledoc """
|
||||||
|
The Languages context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias BoardWise.Repo
|
||||||
|
|
||||||
|
alias BoardWise.Languages.Language
|
||||||
|
|
||||||
|
@prefix "coach_scraper"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of languages.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> list_languages()
|
||||||
|
[%Language{}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
def list_languages do
|
||||||
|
Repo.all(Language, prefix: @prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single language.
|
||||||
|
|
||||||
|
Raises `Ecto.NoResultsError` if the Language does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_language!(123)
|
||||||
|
%Language{}
|
||||||
|
|
||||||
|
iex> get_language!(456)
|
||||||
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_language!(id), do: Repo.get!(Language, id, prefix: @prefix)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a language.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> create_language(%{field: value})
|
||||||
|
{:ok, %Language{}}
|
||||||
|
|
||||||
|
iex> create_language(%{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def create_language(attrs \\ %{}) do
|
||||||
|
%Language{}
|
||||||
|
|> Language.changeset(attrs)
|
||||||
|
|> Repo.insert(prefix: @prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a language.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> update_language(language, %{field: new_value})
|
||||||
|
{:ok, %Language{}}
|
||||||
|
|
||||||
|
iex> update_language(language, %{field: bad_value})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def update_language(%Language{} = language, attrs) do
|
||||||
|
language
|
||||||
|
|> Language.changeset(attrs)
|
||||||
|
|> Repo.update(prefix: @prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes a language.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> delete_language(language)
|
||||||
|
{:ok, %Language{}}
|
||||||
|
|
||||||
|
iex> delete_language(language)
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def delete_language(%Language{} = language) do
|
||||||
|
Repo.delete(language, prefix: @prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for tracking language changes.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> change_language(language)
|
||||||
|
%Ecto.Changeset{data: %Language{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def change_language(%Language{} = language, attrs \\ %{}) do
|
||||||
|
Language.changeset(language, attrs)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule BoardWise.Languages.Language do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "languages" do
|
||||||
|
field :code, :string
|
||||||
|
field :name, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(language, attrs) do
|
||||||
|
language
|
||||||
|
|> cast(attrs, [:code, :name])
|
||||||
|
|> validate_required([:code, :name])
|
||||||
|
|> unique_constraint(:code_unique, name: :code_unique)
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,6 +14,7 @@ defmodule BoardWiseWeb.CoachJSON do
|
||||||
username: coach.username,
|
username: coach.username,
|
||||||
name: coach.name,
|
name: coach.name,
|
||||||
image_url: coach.image_url,
|
image_url: coach.image_url,
|
||||||
|
languages: coach.languages,
|
||||||
rapid: coach.rapid,
|
rapid: coach.rapid,
|
||||||
blitz: coach.blitz,
|
blitz: coach.blitz,
|
||||||
bullet: coach.bullet
|
bullet: coach.bullet
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule BoardWiseWeb.LanguageController do
|
||||||
|
use BoardWiseWeb, :controller
|
||||||
|
|
||||||
|
alias BoardWise.Languages
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
langs = Languages.list_languages()
|
||||||
|
render(conn, :index, langs: langs)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule BoardWiseWeb.LanguageJSON do
|
||||||
|
alias BoardWise.Languages.Language
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a list of coaches.
|
||||||
|
"""
|
||||||
|
def index(%{langs: langs}) do
|
||||||
|
%{data: for(lang <- langs, do: data(lang))}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp data(%Language{} = lang) do
|
||||||
|
%{
|
||||||
|
code: lang.code,
|
||||||
|
name: lang.name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,6 +26,7 @@ defmodule BoardWiseWeb.Router do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
get "/coaches", CoachController, :index
|
get "/coaches", CoachController, :index
|
||||||
|
get "/languages", LanguageController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule BoardWise.Repo.Migrations.CreateLanguages do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
@prefix "coach_scraper"
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:languages, prefix: @prefix) do
|
||||||
|
add :code, :string, null: false
|
||||||
|
add :name, :string, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(
|
||||||
|
:languages,
|
||||||
|
[:code],
|
||||||
|
prefix: @prefix,
|
||||||
|
name: "code_unique"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,61 @@
|
||||||
|
defmodule BoardWise.LanguagesTest do
|
||||||
|
use BoardWise.DataCase
|
||||||
|
|
||||||
|
alias BoardWise.Languages
|
||||||
|
|
||||||
|
describe "languages" do
|
||||||
|
alias BoardWise.Languages.Language
|
||||||
|
|
||||||
|
import BoardWise.LanguagesFixtures
|
||||||
|
|
||||||
|
@invalid_attrs %{code: nil, name: nil}
|
||||||
|
|
||||||
|
test "list_languages/0 returns all languages" do
|
||||||
|
language = language_fixture()
|
||||||
|
assert Languages.list_languages() == [language]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_language!/1 returns the language with given id" do
|
||||||
|
language = language_fixture()
|
||||||
|
assert Languages.get_language!(language.id) == language
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_language/1 with valid data creates a language" do
|
||||||
|
valid_attrs = %{code: "some code", name: "some name"}
|
||||||
|
|
||||||
|
assert {:ok, %Language{} = language} = Languages.create_language(valid_attrs)
|
||||||
|
assert language.code == "some code"
|
||||||
|
assert language.name == "some name"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_language/1 with invalid data returns error changeset" do
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Languages.create_language(@invalid_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update_language/2 with valid data updates the language" do
|
||||||
|
language = language_fixture()
|
||||||
|
update_attrs = %{code: "some updated code", name: "some updated name"}
|
||||||
|
|
||||||
|
assert {:ok, %Language{} = language} = Languages.update_language(language, update_attrs)
|
||||||
|
assert language.code == "some updated code"
|
||||||
|
assert language.name == "some updated name"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update_language/2 with invalid data returns error changeset" do
|
||||||
|
language = language_fixture()
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Languages.update_language(language, @invalid_attrs)
|
||||||
|
assert language == Languages.get_language!(language.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete_language/1 deletes the language" do
|
||||||
|
language = language_fixture()
|
||||||
|
assert {:ok, %Language{}} = Languages.delete_language(language)
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Languages.get_language!(language.id) end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "change_language/1 returns a language changeset" do
|
||||||
|
language = language_fixture()
|
||||||
|
assert %Ecto.Changeset{} = Languages.change_language(language)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule BoardWise.LanguagesFixtures do
|
||||||
|
@moduledoc """
|
||||||
|
This module defines test helpers for creating
|
||||||
|
entities via the `BoardWise.Languages` context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a language.
|
||||||
|
"""
|
||||||
|
def language_fixture(attrs \\ %{}) do
|
||||||
|
{:ok, language} =
|
||||||
|
attrs
|
||||||
|
|> Enum.into(%{
|
||||||
|
code: "some code",
|
||||||
|
name: "some name"
|
||||||
|
})
|
||||||
|
|> BoardWise.Languages.create_language()
|
||||||
|
|
||||||
|
language
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue