Merge pull request #11 from instat/OTT-91-country-list
commit
b548d1e607
|
After Width: | Height: | Size: 250 B |
@ -1,3 +1,4 @@ |
||||
export * from './routes' |
||||
export * from './pages' |
||||
export * from './authKeys' |
||||
export * from './procedures' |
||||
|
||||
@ -0,0 +1,3 @@ |
||||
export const PROCEDURES = { |
||||
lst_c_country: 'lst_c_country', |
||||
} |
||||
@ -1,2 +1,2 @@ |
||||
export const API_ROOT = '' |
||||
export const API_ROOT = 'http://85.10.224.24:8080' |
||||
export const DATA_URL = `${API_ROOT}/data` |
||||
|
||||
@ -0,0 +1,69 @@ |
||||
import { matchSort } from '..' |
||||
|
||||
it('does not sort items when query is empty', () => { |
||||
const items = [ |
||||
{ name: 'app' }, |
||||
{ name: 'apple' }, |
||||
] |
||||
const query = '' |
||||
const result = matchSort( |
||||
items, |
||||
'name', |
||||
query, |
||||
) |
||||
expect(result).toEqual(items) |
||||
}) |
||||
|
||||
it('non matching words omitted', () => { |
||||
const items = [ |
||||
{ name: 'banana' }, |
||||
{ name: 'app' }, |
||||
{ name: 'apple' }, |
||||
] |
||||
const query = 'app' |
||||
const result = matchSort( |
||||
items, |
||||
'name', |
||||
query, |
||||
) |
||||
expect(result).toEqual([ |
||||
items[1], |
||||
items[2], |
||||
]) |
||||
}) |
||||
|
||||
it('matching words go first', () => { |
||||
const items = [ |
||||
{ name: 'apple' }, |
||||
{ name: 'app' }, |
||||
] |
||||
const query = 'app' |
||||
const result = matchSort( |
||||
items, |
||||
'name', |
||||
query, |
||||
) |
||||
expect(result).toEqual([ |
||||
items[1], |
||||
items[0], |
||||
]) |
||||
}) |
||||
|
||||
it('words starting with query word go second', () => { |
||||
const items = [ |
||||
{ name: 'apple' }, |
||||
{ name: 'app' }, |
||||
{ name: 'buggy app' }, |
||||
] |
||||
const query = 'app' |
||||
const result = matchSort( |
||||
items, |
||||
'name', |
||||
query, |
||||
) |
||||
expect(result).toEqual([ |
||||
items[1], |
||||
items[0], |
||||
items[2], |
||||
]) |
||||
}) |
||||
@ -0,0 +1,77 @@ |
||||
import startsWith from 'lodash/startsWith' |
||||
import orderBy from 'lodash/orderBy' |
||||
import toLower from 'lodash/toLower' |
||||
import filter from 'lodash/filter' |
||||
import split from 'lodash/split' |
||||
import size from 'lodash/size' |
||||
import some from 'lodash/some' |
||||
|
||||
type Item = { [key: string]: any } |
||||
|
||||
type Func = <T extends Item, K extends keyof T>( |
||||
items: Array<T>, |
||||
key: K, |
||||
query: string, |
||||
) => Array<T> |
||||
|
||||
const startsWithIgnore = (word: string, target: string) => ( |
||||
startsWith(toLower(word), toLower(target)) |
||||
) |
||||
|
||||
const hasManyWords = (word: string) => size(split(word, ' ')) > 1 |
||||
|
||||
const atLeastOneWordStartsWith = (word: string, target: string) => { |
||||
const words = split(word, ' ') |
||||
return some(words, (w) => startsWithIgnore(w, target)) |
||||
} |
||||
|
||||
const filterMatching: Func = ( |
||||
items, |
||||
key, |
||||
query, |
||||
) => ( |
||||
filter(items, (item) => { |
||||
const value = item[key] |
||||
if (startsWithIgnore(value, query)) return true |
||||
if (hasManyWords(value)) return atLeastOneWordStartsWith(value, query) |
||||
return false |
||||
}) |
||||
) |
||||
|
||||
const sortMatching: Func = ( |
||||
items, |
||||
key, |
||||
query, |
||||
) => ( |
||||
orderBy( |
||||
items, |
||||
(item) => { |
||||
if (item[key] === query) return 10 |
||||
if (startsWithIgnore(item[key], query)) { |
||||
return 5 |
||||
} |
||||
return 0 |
||||
}, |
||||
'desc', |
||||
) |
||||
) |
||||
|
||||
export const matchSort: Func = ( |
||||
items, |
||||
key, |
||||
query, |
||||
) => { |
||||
if (!query) return items |
||||
|
||||
const filteredItems = filterMatching( |
||||
items, |
||||
key, |
||||
query, |
||||
) |
||||
|
||||
return sortMatching( |
||||
filteredItems, |
||||
key, |
||||
query, |
||||
) |
||||
} |
||||
@ -0,0 +1,88 @@ |
||||
import type { ChangeEvent, FocusEvent } from 'react' |
||||
import type { Countries } from 'requests' |
||||
|
||||
import { |
||||
useEffect, |
||||
useState, |
||||
useCallback, |
||||
} from 'react' |
||||
|
||||
import toLower from 'lodash/toLower' |
||||
import slice from 'lodash/slice' |
||||
import find from 'lodash/find' |
||||
|
||||
import { getCountries } from 'requests' |
||||
|
||||
import { matchSort } from './helpers' |
||||
|
||||
// временно, будем считывать из стора(контекста) лексики
|
||||
const useCurrentLang = () => 'eng' |
||||
|
||||
const useCountriesList = () => { |
||||
const [countries, setCountries] = useState<Countries>([]) |
||||
|
||||
useEffect(() => { |
||||
getCountries().then(setCountries) |
||||
}, []) |
||||
|
||||
return countries |
||||
} |
||||
|
||||
const useQuery = () => { |
||||
const [query, setQuery] = useState('') |
||||
|
||||
const onQueryChange = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => { |
||||
setQuery(target.value) |
||||
}, []) |
||||
|
||||
return { |
||||
onQueryChange, |
||||
query, |
||||
setQuery, |
||||
} |
||||
} |
||||
|
||||
const isOptionClicked = (target: HTMLElement) => target?.getAttribute('role') === 'option' |
||||
|
||||
export const useCountrySelector = () => { |
||||
const lang = useCurrentLang() |
||||
const countries = useCountriesList() |
||||
const { |
||||
onQueryChange, |
||||
query, |
||||
setQuery, |
||||
} = useQuery() |
||||
|
||||
const keyToSortBy = `name_${lang}` as 'name_eng' |
||||
|
||||
const results = matchSort( |
||||
countries, |
||||
keyToSortBy, |
||||
query, |
||||
) |
||||
|
||||
const onBlur = (event: FocusEvent<HTMLInputElement>) => { |
||||
const target = event.relatedTarget as HTMLElement | null |
||||
// клик по элементу списка тоже вызывает onBlur
|
||||
// если кликали элемент списка то событие обрабатывается onCountrySelect
|
||||
if (target && isOptionClicked(target)) return |
||||
|
||||
const found = find( |
||||
countries, |
||||
(country) => toLower(country[keyToSortBy]) === toLower(query), |
||||
) |
||||
setQuery(found ? found[keyToSortBy] : '') |
||||
} |
||||
|
||||
return { |
||||
countries: slice( |
||||
results, |
||||
0, |
||||
20, |
||||
), |
||||
onBlur, |
||||
onCountrySelect: setQuery, |
||||
onQueryChange, |
||||
query, |
||||
} |
||||
} |
||||
@ -0,0 +1,63 @@ |
||||
import React from 'react' |
||||
|
||||
import isEmpty from 'lodash/isEmpty' |
||||
import map from 'lodash/map' |
||||
import { |
||||
ComboboxPopover, |
||||
ComboboxList, |
||||
ComboboxOption, |
||||
} from '@reach/combobox' |
||||
import '@reach/combobox/styles.css' |
||||
|
||||
import { Label } from 'features/Common/Input/styled' |
||||
|
||||
import { useCountrySelector } from './hooks' |
||||
import { |
||||
ComboboxStyled, |
||||
ComboboxInputStyled, |
||||
Arrow, |
||||
} from './styled' |
||||
|
||||
type Props = { |
||||
labelWidth?: number, |
||||
} |
||||
|
||||
export const CountrySelector = ({ labelWidth }: Props) => { |
||||
const { |
||||
countries, |
||||
onBlur, |
||||
onCountrySelect, |
||||
onQueryChange, |
||||
query, |
||||
} = useCountrySelector() |
||||
|
||||
return ( |
||||
<ComboboxStyled onSelect={onCountrySelect}> |
||||
<Label |
||||
labelWidth={labelWidth} |
||||
htmlFor='country' |
||||
> |
||||
Страна |
||||
<Arrow /> |
||||
</Label> |
||||
<ComboboxInputStyled |
||||
id='country' |
||||
value={query} |
||||
onChange={onQueryChange} |
||||
onBlur={onBlur} |
||||
/> |
||||
{!isEmpty(countries) && ( |
||||
<ComboboxPopover> |
||||
<ComboboxList> |
||||
{map(countries, ({ id, name_eng }) => ( |
||||
<ComboboxOption |
||||
key={id} |
||||
value={name_eng} |
||||
/> |
||||
))} |
||||
</ComboboxList> |
||||
</ComboboxPopover> |
||||
)} |
||||
</ComboboxStyled> |
||||
) |
||||
} |
||||
@ -0,0 +1,27 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { Combobox, ComboboxInput } from '@reach/combobox' |
||||
|
||||
import { wrapperStyles, inputStyles } from 'features/Common/Input/styled' |
||||
|
||||
export const ComboboxStyled = styled(Combobox)` |
||||
${wrapperStyles} |
||||
position: relative; |
||||
` |
||||
|
||||
export const ComboboxInputStyled = styled(ComboboxInput)` |
||||
${inputStyles} |
||||
padding-right: 24px; |
||||
` |
||||
|
||||
export const Arrow = styled.div` |
||||
position: absolute; |
||||
right: 20px; |
||||
top: 50%; |
||||
transform: translateY(-50%); |
||||
width: 12px; |
||||
height: 12px; |
||||
background-image: url(/images/arrowDown.svg); |
||||
background-position: center; |
||||
background-repeat: no-repeat; |
||||
` |
||||
@ -1,3 +1,3 @@ |
||||
export const getResponseData = (proc: string) => (response: any) => ( |
||||
response?.data?.[0]?.[proc] |
||||
response?.[proc] |
||||
) |
||||
|
||||
@ -1 +1,2 @@ |
||||
export * from './callApi' |
||||
export * from './callApi/getResponseData' |
||||
|
||||
@ -0,0 +1,28 @@ |
||||
import { DATA_URL, PROCEDURES } from 'config' |
||||
import { callApi, getResponseData } from 'helpers' |
||||
|
||||
const proc = PROCEDURES.lst_c_country |
||||
|
||||
export type Country = { |
||||
id: number, |
||||
iso_3166_1_alpha_2: string, |
||||
iso_3166_1_alpha_3: string, |
||||
name_eng: string, |
||||
name_rus: string, |
||||
} |
||||
|
||||
export type Countries = Array<Country> |
||||
|
||||
export const getCountries = (): Promise<Countries> => { |
||||
const config = { |
||||
body: { |
||||
params: null, |
||||
proc, |
||||
}, |
||||
} |
||||
|
||||
return callApi({ |
||||
config, |
||||
url: DATA_URL, |
||||
}).then(getResponseData(proc)) |
||||
} |
||||
@ -1 +1,2 @@ |
||||
export * from './login' |
||||
export * from './getCountries' |
||||
|
||||
Loading…
Reference in new issue