diff --git a/package.json b/package.json
index a0e041a5..fd673ac5 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/images/arrowDown.svg b/public/images/arrowDown.svg
new file mode 100644
index 00000000..94f5058a
--- /dev/null
+++ b/public/images/arrowDown.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/config/index.tsx b/src/config/index.tsx
index bf81f61f..e5851bad 100644
--- a/src/config/index.tsx
+++ b/src/config/index.tsx
@@ -1,3 +1,4 @@
export * from './routes'
export * from './pages'
export * from './authKeys'
+export * from './procedures'
diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx
new file mode 100644
index 00000000..6138575d
--- /dev/null
+++ b/src/config/procedures.tsx
@@ -0,0 +1,3 @@
+export const PROCEDURES = {
+ lst_c_country: 'lst_c_country',
+}
diff --git a/src/config/routes.tsx b/src/config/routes.tsx
index 7cc14d50..708156ee 100644
--- a/src/config/routes.tsx
+++ b/src/config/routes.tsx
@@ -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`
diff --git a/src/features/Common/Input/styled.tsx b/src/features/Common/Input/styled.tsx
index 708cd621..873080ba 100644
--- a/src/features/Common/Input/styled.tsx
+++ b/src/features/Common/Input/styled.tsx
@@ -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`
+export const wrapperStyles = css`
width: ${({ wrapperWidth }) => (wrapperWidth ? `${wrapperWidth}px` : '100%')};
height: 48px;
margin: 20px 0;
@@ -20,6 +20,10 @@ export const InputWrapper = styled.div`
border-radius: 2px;
`
+export const InputWrapper = styled.div`
+ ${wrapperStyles}
+`
+
type TLabel = {
labelWidth?: number,
}
@@ -39,7 +43,7 @@ type TInputStyled = {
inputWidth?: number,
}
-export const InputStyled = styled.input`
+export const inputStyles = css`
flex-grow: 1;
font-weight: bold;
font-size: 20px;
@@ -68,3 +72,7 @@ export const InputStyled = styled.input`
-webkit-text-fill-color: ${({ theme: { colors } }) => colors.text};
}
`
+
+export const InputStyled = styled.input`
+ ${inputStyles}
+`
diff --git a/src/features/CountrySelector/helpers/__tests__/index.tsx b/src/features/CountrySelector/helpers/__tests__/index.tsx
new file mode 100644
index 00000000..1b78ecbb
--- /dev/null
+++ b/src/features/CountrySelector/helpers/__tests__/index.tsx
@@ -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],
+ ])
+})
diff --git a/src/features/CountrySelector/helpers/index.tsx b/src/features/CountrySelector/helpers/index.tsx
new file mode 100644
index 00000000..e891445b
--- /dev/null
+++ b/src/features/CountrySelector/helpers/index.tsx
@@ -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 = (
+ items: Array,
+ key: K,
+ query: string,
+) => Array
+
+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,
+ )
+}
diff --git a/src/features/CountrySelector/hooks.tsx b/src/features/CountrySelector/hooks.tsx
new file mode 100644
index 00000000..61a69061
--- /dev/null
+++ b/src/features/CountrySelector/hooks.tsx
@@ -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([])
+
+ useEffect(() => {
+ getCountries().then(setCountries)
+ }, [])
+
+ return countries
+}
+
+const useQuery = () => {
+ const [query, setQuery] = useState('')
+
+ const onQueryChange = useCallback(({ target }: ChangeEvent) => {
+ 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) => {
+ 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,
+ }
+}
diff --git a/src/features/CountrySelector/index.tsx b/src/features/CountrySelector/index.tsx
new file mode 100644
index 00000000..07912710
--- /dev/null
+++ b/src/features/CountrySelector/index.tsx
@@ -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 (
+
+
+
+ {!isEmpty(countries) && (
+
+
+ {map(countries, ({ id, name_eng }) => (
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/src/features/CountrySelector/styled.tsx b/src/features/CountrySelector/styled.tsx
new file mode 100644
index 00000000..bcd76ac8
--- /dev/null
+++ b/src/features/CountrySelector/styled.tsx
@@ -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;
+`
diff --git a/src/features/Register/Steps/Registration/index.tsx b/src/features/Register/Steps/Registration/index.tsx
index 9e5a5fe8..3eefccde 100644
--- a/src/features/Register/Steps/Registration/index.tsx
+++ b/src/features/Register/Steps/Registration/index.tsx
@@ -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 */}
+
diff --git a/src/helpers/callApi/getResponseData.tsx b/src/helpers/callApi/getResponseData.tsx
index 149ace17..2aa97ed1 100644
--- a/src/helpers/callApi/getResponseData.tsx
+++ b/src/helpers/callApi/getResponseData.tsx
@@ -1,3 +1,3 @@
export const getResponseData = (proc: string) => (response: any) => (
- response?.data?.[0]?.[proc]
+ response?.[proc]
)
diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx
index 8842e651..1ba6128d 100644
--- a/src/helpers/index.tsx
+++ b/src/helpers/index.tsx
@@ -1 +1,2 @@
export * from './callApi'
+export * from './callApi/getResponseData'
diff --git a/src/requests/getCountries.tsx b/src/requests/getCountries.tsx
new file mode 100644
index 00000000..30944b42
--- /dev/null
+++ b/src/requests/getCountries.tsx
@@ -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
+
+export const getCountries = (): Promise => {
+ const config = {
+ body: {
+ params: null,
+ proc,
+ },
+ }
+
+ return callApi({
+ config,
+ url: DATA_URL,
+ }).then(getResponseData(proc))
+}
diff --git a/src/requests/index.tsx b/src/requests/index.tsx
index d4389750..a803e3e4 100644
--- a/src/requests/index.tsx
+++ b/src/requests/index.tsx
@@ -1 +1,2 @@
export * from './login'
+export * from './getCountries'