Merge pull request #11 from instat/OTT-91-country-list

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Andrey Razdorskiy 6 years ago committed by GitHub
commit b548d1e607
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      package.json
  2. 4
      public/images/arrowDown.svg
  3. 1
      src/config/index.tsx
  4. 3
      src/config/procedures.tsx
  5. 2
      src/config/routes.tsx
  6. 14
      src/features/Common/Input/styled.tsx
  7. 69
      src/features/CountrySelector/helpers/__tests__/index.tsx
  8. 77
      src/features/CountrySelector/helpers/index.tsx
  9. 88
      src/features/CountrySelector/hooks.tsx
  10. 63
      src/features/CountrySelector/index.tsx
  11. 27
      src/features/CountrySelector/styled.tsx
  12. 11
      src/features/Register/Steps/Registration/index.tsx
  13. 2
      src/helpers/callApi/getResponseData.tsx
  14. 1
      src/helpers/index.tsx
  15. 28
      src/requests/getCountries.tsx
  16. 1
      src/requests/index.tsx

@ -13,6 +13,7 @@
"build-storybook": "build-storybook -s public"
},
"dependencies": {
"@reach/combobox": "^0.10.4",
"history": "^4.10.1",
"lodash": "^4.17.15",
"react": "^16.13.1",

@ -0,0 +1,4 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6 0.600098L6 5.2001L1.4 0.600098L0 2.0001L6 8.0001L12 2.0001L10.6 0.600098Z" fill="#999999"/>
</svg>

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`

@ -1,11 +1,11 @@
import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'
export type TInputWrapper = {
paddingX?: number,
wrapperWidth?: number,
}
export const InputWrapper = styled.div<TInputWrapper>`
export const wrapperStyles = css<TInputWrapper>`
width: ${({ wrapperWidth }) => (wrapperWidth ? `${wrapperWidth}px` : '100%')};
height: 48px;
margin: 20px 0;
@ -20,6 +20,10 @@ export const InputWrapper = styled.div<TInputWrapper>`
border-radius: 2px;
`
export const InputWrapper = styled.div<TInputWrapper>`
${wrapperStyles}
`
type TLabel = {
labelWidth?: number,
}
@ -39,7 +43,7 @@ type TInputStyled = {
inputWidth?: number,
}
export const InputStyled = styled.input<TInputStyled>`
export const inputStyles = css<TInputStyled>`
flex-grow: 1;
font-weight: bold;
font-size: 20px;
@ -68,3 +72,7 @@ export const InputStyled = styled.input<TInputStyled>`
-webkit-text-fill-color: ${({ theme: { colors } }) => colors.text};
}
`
export const InputStyled = styled.input<TInputStyled>`
${inputStyles}
`

@ -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;
`

@ -2,6 +2,7 @@ import React from 'react'
import { PAGES } from 'config'
import { CountrySelector } from 'features/CountrySelector'
import { Input } from 'features/Common'
import {
BlockTitle,
@ -38,15 +39,15 @@ export const Registration = () => (
label='E-mail'
labelWidth={labelWidth}
/>
{/* TODO: it should be Dropdown */}
<CountrySelector labelWidth={78} />
<Input
id='country'
label='Страна'
id='address1'
label='Адрес 1'
labelWidth={labelWidth}
/>
<Input
id='address'
label='Адрес'
id='address2'
label='Адрес 2'
labelWidth={labelWidth}
/>

@ -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…
Cancel
Save