Ott 102 search (#40)
parent
5bfa0d7b7e
commit
c23e650512
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,5 @@ |
|||||||
|
export const SPORT_COLORS = { |
||||||
|
basketball: '#f1903b', |
||||||
|
football: '#00a435', |
||||||
|
hockey: '#5eb1ff', |
||||||
|
} |
||||||
@ -1,4 +1,10 @@ |
|||||||
export const authenticatedLexics = { |
export const authenticatedLexics = { |
||||||
|
basketball: 6960, |
||||||
|
football: 6958, |
||||||
|
hockey: 6959, |
||||||
logout: 4306, |
logout: 4306, |
||||||
|
player: 630, |
||||||
|
team: 658, |
||||||
|
tournament: 1009, |
||||||
user_account: 12928, |
user_account: 12928, |
||||||
} |
} |
||||||
|
|||||||
@ -1,2 +1,22 @@ |
|||||||
export const API_ROOT = 'http://api-staging.instat.tv' |
export const API_ROOT = 'http://api-staging.instat.tv' |
||||||
export const DATA_URL = `${API_ROOT}/data` |
export const DATA_URL = `${API_ROOT}/data` |
||||||
|
|
||||||
|
export const LOGOS_FALLBACKS = { |
||||||
|
basketball: { |
||||||
|
players: 'https://basketball.instatscout.com/images/player-no-photo.png', |
||||||
|
teams: 'https://basketball.instatscout.com/images/team-no-photo.png', |
||||||
|
tournaments: 'https://basketball.instatscout.com/images/tournaments/180/no-photo.png', |
||||||
|
}, |
||||||
|
|
||||||
|
football: { |
||||||
|
players: 'https://football.instatscout.com/images/player-no-photo.png', |
||||||
|
teams: 'https://football.instatscout.com/images/team-no-photo.png', |
||||||
|
tournaments: 'https://football.instatscout.com/images/tournament-no-photo.png', |
||||||
|
}, |
||||||
|
|
||||||
|
hockey: { |
||||||
|
players: 'https://hockey.instatscout.com/images/player-no-photo.png', |
||||||
|
teams: 'https://hockey.instatscout.com/images/team-no-photo.png', |
||||||
|
tournaments: 'https://hockey.instatscout.com/images/tournaments/180/no-photo.png', |
||||||
|
}, |
||||||
|
} |
||||||
|
|||||||
@ -0,0 +1,22 @@ |
|||||||
|
import { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
export const сustomScrollbar = css` |
||||||
|
::-webkit-scrollbar { |
||||||
|
width: 8px; |
||||||
|
height: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb { |
||||||
|
border-radius: 6px; |
||||||
|
background: #3F3F3F; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-button { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-track, |
||||||
|
::-webkit-scrollbar-corner { |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
` |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
import { |
||||||
|
SyntheticEvent, |
||||||
|
useEffect, |
||||||
|
useRef, |
||||||
|
useCallback, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import forEach from 'lodash/forEach' |
||||||
|
|
||||||
|
export const useItemsList = () => { |
||||||
|
const ref = useRef<HTMLUListElement>(null) |
||||||
|
|
||||||
|
const onError = useCallback((fallbackImage: string) => (e: SyntheticEvent<HTMLImageElement>) => { |
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
e.currentTarget.src = fallbackImage |
||||||
|
}, []) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (ref.current) { |
||||||
|
/** |
||||||
|
* Реализуем ленивую загрузку изображений |
||||||
|
* для оптимизации запросов к серверу |
||||||
|
*/ |
||||||
|
|
||||||
|
// устанавливаем настройки
|
||||||
|
const options = { |
||||||
|
// область просмотра
|
||||||
|
root: ref.current.parentElement, |
||||||
|
// процент пересечения с областью просмотра - половина изображения
|
||||||
|
threshold: 0.5, |
||||||
|
} |
||||||
|
|
||||||
|
// создаем наблюдатель
|
||||||
|
const intersectionObserver = new IntersectionObserver((entries, observer) => { |
||||||
|
// для каждой записи-целевого элемента
|
||||||
|
forEach(entries, (entry) => { |
||||||
|
// если элемент является наблюдаемым
|
||||||
|
if (entry.isIntersecting) { |
||||||
|
const imgElem = entry.target as HTMLImageElement |
||||||
|
// устанавливаем аттрибут src из data-src
|
||||||
|
imgElem.src = imgElem.dataset.src! |
||||||
|
|
||||||
|
// прекращаем наблюдение
|
||||||
|
observer.unobserve(imgElem) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, options) |
||||||
|
|
||||||
|
const imgElems = ref.current.getElementsByTagName('img') |
||||||
|
|
||||||
|
// находим все элементы img и делаем наблюдаемыми
|
||||||
|
forEach(imgElems, (imgElem) => { |
||||||
|
intersectionObserver.observe(imgElem) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
onError, |
||||||
|
ref, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import map from 'lodash/map' |
||||||
|
|
||||||
|
import { SPORT_COLORS } from 'config' |
||||||
|
import type { SportType } from 'features/Search/config' |
||||||
|
|
||||||
|
import { useItemsList } from './hooks' |
||||||
|
import { |
||||||
|
Logo, |
||||||
|
LogoWrapper, |
||||||
|
Item, |
||||||
|
ItemInfo, |
||||||
|
Name, |
||||||
|
SportName, |
||||||
|
StyledLink, |
||||||
|
TeamOrCountry, |
||||||
|
Wrapper, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type SearchItemsListProps = { |
||||||
|
list: Array<{ |
||||||
|
fallbackImage: string, |
||||||
|
id: number, |
||||||
|
logo: string, |
||||||
|
name: string, |
||||||
|
profileUrl: string, |
||||||
|
sportType: SportType, |
||||||
|
teamOrCountry?: string, |
||||||
|
}>, |
||||||
|
} |
||||||
|
|
||||||
|
export const ItemsList = ({ list }: SearchItemsListProps) => { |
||||||
|
const { |
||||||
|
onError, |
||||||
|
ref, |
||||||
|
} = useItemsList() |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper ref={ref}> |
||||||
|
{map(list, ({ |
||||||
|
fallbackImage, |
||||||
|
id, |
||||||
|
logo, |
||||||
|
name, |
||||||
|
profileUrl, |
||||||
|
sportType, |
||||||
|
teamOrCountry, |
||||||
|
}) => ( |
||||||
|
<Item key={id}> |
||||||
|
<StyledLink to={profileUrl} target='_blank'> |
||||||
|
<LogoWrapper> |
||||||
|
<Logo |
||||||
|
data-src={logo} |
||||||
|
onError={onError(fallbackImage)} |
||||||
|
alt={name} |
||||||
|
/> |
||||||
|
</LogoWrapper> |
||||||
|
<ItemInfo> |
||||||
|
<Name>{name}</Name> |
||||||
|
<SportName |
||||||
|
color={SPORT_COLORS[sportType]} |
||||||
|
t={sportType} |
||||||
|
/> |
||||||
|
<TeamOrCountry>{teamOrCountry}</TeamOrCountry> |
||||||
|
</ItemInfo> |
||||||
|
</StyledLink> |
||||||
|
</Item> |
||||||
|
))} |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
import { Link } from 'react-router-dom' |
||||||
|
|
||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
export const Wrapper = styled.ul` |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
list-style-type: none; |
||||||
|
` |
||||||
|
|
||||||
|
export const Item = styled.li` |
||||||
|
:focus { |
||||||
|
background-color: #999; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const StyledLink = styled(Link)` |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
height: 56px; |
||||||
|
background-color: #666; |
||||||
|
|
||||||
|
:focus-within,
|
||||||
|
:hover { |
||||||
|
background-color: #999; |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const ItemInfo = styled.div` |
||||||
|
line-height: 17px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Name = styled.div` |
||||||
|
font-size: 16px; |
||||||
|
font-weight: bold; |
||||||
|
color: #ccc; |
||||||
|
` |
||||||
|
|
||||||
|
export const SportName = styled(T9n)<{ color: string }>` |
||||||
|
margin-right: 10px; |
||||||
|
font-size: 11px; |
||||||
|
color: ${({ color }) => color}; |
||||||
|
letter-spacing: 0.02em; |
||||||
|
text-transform: uppercase; |
||||||
|
` |
||||||
|
|
||||||
|
export const TeamOrCountry = styled.span` |
||||||
|
font-size: 11px; |
||||||
|
color: #ccc; |
||||||
|
` |
||||||
|
|
||||||
|
export const LogoWrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
flex-shrink: 0; |
||||||
|
width: 55px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Logo = styled.img` |
||||||
|
width: 24px; |
||||||
|
height: 24px; |
||||||
|
` |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
Icon, |
||||||
|
titleStyles, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type HeaderProps = { |
||||||
|
image: string, |
||||||
|
title: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const Header = ({ image, title }: HeaderProps) => ( |
||||||
|
<Wrapper> |
||||||
|
<Icon image={image} /> |
||||||
|
<T9n t={title} customStyles={titleStyles} /> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
padding-left: 20px; |
||||||
|
background-color: #005EDD; |
||||||
|
` |
||||||
|
|
||||||
|
export const Icon = styled.div<{ image: string }>` |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
margin-right: 10px; |
||||||
|
background-image: url(/images/${({ image }) => `${image}.svg`}); |
||||||
|
background-size: 100%; |
||||||
|
background-repeat: no-repeat; |
||||||
|
` |
||||||
|
|
||||||
|
export const titleStyles = css` |
||||||
|
letter-spacing: 0.02em; |
||||||
|
text-transform: uppercase; |
||||||
|
color: #fff; |
||||||
|
` |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
export const SEARCH_DELAY = 1500 // Задержка поиска в мс
|
||||||
|
|
||||||
|
export const MIN_CHARACTERS_LENGTH = 3 // Минимальное число символов для поиска
|
||||||
|
|
||||||
|
export type SportType = 'football' | 'basketball' | 'hockey' |
||||||
|
|
||||||
|
export const SPORT_TYPES = { |
||||||
|
1: 'football', |
||||||
|
2: 'hockey', |
||||||
|
3: 'basketball', |
||||||
|
} as const |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
import type { FormEvent, ChangeEvent } from 'react' |
||||||
|
import { |
||||||
|
useState, |
||||||
|
FocusEvent, |
||||||
|
useRef, |
||||||
|
useCallback, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import trim from 'lodash/trim' |
||||||
|
import debounce from 'lodash/debounce' |
||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
import size from 'lodash/size' |
||||||
|
|
||||||
|
import type { SearchItems } from 'requests' |
||||||
|
import { getSearchItems } from 'requests' |
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
import { SEARCH_DELAY, MIN_CHARACTERS_LENGTH } from '../config' |
||||||
|
import { useNormalizedItems } from './useNormalizedItems' |
||||||
|
|
||||||
|
export const useSearch = () => { |
||||||
|
const [searchItems, setSearchItems] = useState<SearchItems>({}) |
||||||
|
const abortControllerRef = useRef<AbortController | null>(null) |
||||||
|
const { |
||||||
|
close, |
||||||
|
isOpen, |
||||||
|
open, |
||||||
|
} = useToggle() |
||||||
|
|
||||||
|
const fetchSearchItems = useCallback(debounce((searchString: string) => { |
||||||
|
const abortController = new window.AbortController() |
||||||
|
abortControllerRef.current = abortController |
||||||
|
|
||||||
|
getSearchItems(searchString, abortController.signal).then((data) => { |
||||||
|
setSearchItems(data) |
||||||
|
abortControllerRef.current = null |
||||||
|
}) |
||||||
|
}, SEARCH_DELAY), []) |
||||||
|
|
||||||
|
const cancelRequest = useCallback(() => { |
||||||
|
const abortController = abortControllerRef.current |
||||||
|
|
||||||
|
if (abortController) { |
||||||
|
abortController.abort() |
||||||
|
abortControllerRef.current = null |
||||||
|
} |
||||||
|
}, []) |
||||||
|
|
||||||
|
const onChange = useCallback(({ target: { value } }: ChangeEvent<HTMLInputElement>) => { |
||||||
|
const trimmedValue = trim(value) |
||||||
|
|
||||||
|
if (size(trimmedValue) >= MIN_CHARACTERS_LENGTH) { |
||||||
|
cancelRequest() |
||||||
|
setSearchItems({}) |
||||||
|
fetchSearchItems(trimmedValue) |
||||||
|
open() |
||||||
|
} else { |
||||||
|
close() |
||||||
|
} |
||||||
|
}, [ |
||||||
|
cancelRequest, |
||||||
|
close, |
||||||
|
fetchSearchItems, |
||||||
|
open, |
||||||
|
]) |
||||||
|
|
||||||
|
const onFocus = useCallback(({ target: { value } }: FocusEvent<HTMLInputElement>) => { |
||||||
|
if (size(value) >= MIN_CHARACTERS_LENGTH) { |
||||||
|
open() |
||||||
|
} |
||||||
|
}, [open]) |
||||||
|
|
||||||
|
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => { |
||||||
|
e.preventDefault() |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
close, |
||||||
|
normalizedItems: useNormalizedItems(searchItems), |
||||||
|
onChange, |
||||||
|
onFocus, |
||||||
|
onSubmit, |
||||||
|
showResults: isOpen && !isEmpty(searchItems), |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,104 @@ |
|||||||
|
import map from 'lodash/map' |
||||||
|
|
||||||
|
import type { SearchItems } from 'requests' |
||||||
|
import { LOGOS_FALLBACKS } from 'config' |
||||||
|
import { getLogo } from 'helpers' |
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
import { SPORT_TYPES } from '../config' |
||||||
|
|
||||||
|
type Firstname = 'firstname_eng' | 'firstname_rus' |
||||||
|
type Lastname = 'lastname_eng' | 'lastname_rus' |
||||||
|
type Name = 'name_eng' | 'name_rus' |
||||||
|
|
||||||
|
export const useNormalizedItems = (searchItems: SearchItems) => { |
||||||
|
const { |
||||||
|
suffix, |
||||||
|
} = useLexicsStore() |
||||||
|
|
||||||
|
const players = map(searchItems.players, (player) => { |
||||||
|
const type = 'players' |
||||||
|
|
||||||
|
const firstName = player[`firstname_${suffix}` as Firstname] |
||||||
|
const lastName = player[`lastname_${suffix}` as Lastname] |
||||||
|
const teamName = player.team?.[`name_${suffix}` as Name] |
||||||
|
|
||||||
|
const sportType = SPORT_TYPES[player.sport] |
||||||
|
|
||||||
|
const { id } = player |
||||||
|
|
||||||
|
const logo = getLogo({ |
||||||
|
id, |
||||||
|
sportType, |
||||||
|
type, |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
fallbackImage: LOGOS_FALLBACKS[sportType].players, |
||||||
|
id, |
||||||
|
logo, |
||||||
|
name: `${firstName} ${lastName}`, |
||||||
|
profileUrl: `/players/${id}`, |
||||||
|
sportType, |
||||||
|
teamOrCountry: teamName, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const teams = map(searchItems.teams, (team) => { |
||||||
|
const name = team[`name_${suffix}` as Name] |
||||||
|
|
||||||
|
const sportType = SPORT_TYPES[team.sport] |
||||||
|
|
||||||
|
const { id } = team |
||||||
|
|
||||||
|
const logo = getLogo({ |
||||||
|
id, |
||||||
|
sportType, |
||||||
|
type: 'teams', |
||||||
|
}) |
||||||
|
|
||||||
|
const country = team.country?.[`name_${suffix}` as Name] |
||||||
|
|
||||||
|
return { |
||||||
|
fallbackImage: LOGOS_FALLBACKS[sportType].teams, |
||||||
|
id, |
||||||
|
logo, |
||||||
|
name, |
||||||
|
profileUrl: `/teams/${id}`, |
||||||
|
sportType, |
||||||
|
teamOrCountry: country, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const tournaments = map(searchItems.tournaments, (tournament) => { |
||||||
|
const name = tournament[`name_${suffix}` as Name] |
||||||
|
|
||||||
|
const sportType = SPORT_TYPES[tournament.sport] |
||||||
|
|
||||||
|
const { id } = tournament |
||||||
|
|
||||||
|
const logo = getLogo({ |
||||||
|
id, |
||||||
|
sportType, |
||||||
|
type: 'tournaments', |
||||||
|
}) |
||||||
|
|
||||||
|
const country = tournament.country?.[`name_${suffix}` as Name] |
||||||
|
|
||||||
|
return { |
||||||
|
fallbackImage: LOGOS_FALLBACKS[sportType].tournaments, |
||||||
|
id, |
||||||
|
logo, |
||||||
|
name, |
||||||
|
profileUrl: `/tournaments/${id}`, |
||||||
|
sportType, |
||||||
|
teamOrCountry: country, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
players, |
||||||
|
teams, |
||||||
|
tournaments, |
||||||
|
} |
||||||
|
} |
||||||
@ -1,5 +1,69 @@ |
|||||||
import React from 'react' |
import React, { Fragment } from 'react' |
||||||
|
|
||||||
import { Wrapper } from './styled' |
import isEmpty from 'lodash/isEmpty' |
||||||
|
|
||||||
export const Search = () => <Wrapper /> |
import { Input } from 'features/Common' |
||||||
|
import { ItemsList } from 'features/ItemsList' |
||||||
|
import { OutsideClick } from 'features/OutsideClick' |
||||||
|
|
||||||
|
import { useSearch } from './hooks' |
||||||
|
import { Header } from './components/Header' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
Form, |
||||||
|
Results, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const Search = () => { |
||||||
|
const { |
||||||
|
close, |
||||||
|
normalizedItems: { |
||||||
|
players, |
||||||
|
teams, |
||||||
|
tournaments, |
||||||
|
}, |
||||||
|
onChange, |
||||||
|
onFocus, |
||||||
|
onSubmit, |
||||||
|
showResults, |
||||||
|
} = useSearch() |
||||||
|
|
||||||
|
return ( |
||||||
|
<OutsideClick onClick={close}> |
||||||
|
<Wrapper> |
||||||
|
<Form role='search' onSubmit={onSubmit}> |
||||||
|
<Input |
||||||
|
id='searchInput' |
||||||
|
type='search' |
||||||
|
onChange={onChange} |
||||||
|
onFocus={onFocus} |
||||||
|
/> |
||||||
|
</Form> |
||||||
|
{showResults && ( |
||||||
|
<Results> |
||||||
|
{!isEmpty(tournaments) && ( |
||||||
|
<Fragment> |
||||||
|
<Header title='tournament' image='tournament' /> |
||||||
|
<ItemsList list={tournaments} /> |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isEmpty(teams) && ( |
||||||
|
<Fragment> |
||||||
|
<Header title='team' image='team' /> |
||||||
|
<ItemsList list={teams} /> |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isEmpty(players) && ( |
||||||
|
<Fragment> |
||||||
|
<Header title='player' image='player' /> |
||||||
|
<ItemsList list={players} /> |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
</Results> |
||||||
|
)} |
||||||
|
</Wrapper> |
||||||
|
</OutsideClick> |
||||||
|
) |
||||||
|
} |
||||||
|
|||||||
@ -1,3 +1,64 @@ |
|||||||
import styled from 'styled-components/macro' |
import styled from 'styled-components/macro' |
||||||
|
|
||||||
export const Wrapper = styled.div`` |
import { сustomScrollbar } from 'features/Common' |
||||||
|
import { |
||||||
|
InputWrapper, |
||||||
|
InputStyled, |
||||||
|
Label, |
||||||
|
} from 'features/Common/Input/styled' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
position: relative; |
||||||
|
` |
||||||
|
|
||||||
|
export const Form = styled.form` |
||||||
|
${InputWrapper} { |
||||||
|
margin: 0; |
||||||
|
padding-bottom: 13px; |
||||||
|
} |
||||||
|
|
||||||
|
${InputStyled} { |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
::-webkit-search-decoration, |
||||||
|
::-webkit-search-cancel-button, |
||||||
|
::-webkit-search-results-button, |
||||||
|
::-webkit-search-results-decoration { |
||||||
|
display: none;
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
${Label} { |
||||||
|
::before { |
||||||
|
content: ''; |
||||||
|
display: block; |
||||||
|
width: 25px; |
||||||
|
height: 25px; |
||||||
|
background-image: url(/images/search.svg); |
||||||
|
background-repeat: no-repeat; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
:focus-within { |
||||||
|
${InputWrapper} { |
||||||
|
padding-left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
${Label} { |
||||||
|
::before { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
` |
||||||
|
|
||||||
|
export const Results = styled.div` |
||||||
|
position: absolute; |
||||||
|
top: 56px; |
||||||
|
width: 448px; |
||||||
|
max-height: 431px; |
||||||
|
overflow-y: auto; |
||||||
|
|
||||||
|
${сustomScrollbar} |
||||||
|
` |
||||||
|
|||||||
@ -0,0 +1,23 @@ |
|||||||
|
import { getLogo } from '..' |
||||||
|
|
||||||
|
describe('getLogo helper', () => { |
||||||
|
it('returns logo url', () => { |
||||||
|
expect(getLogo({ |
||||||
|
id: 1, |
||||||
|
sportType: 'football', |
||||||
|
type: 'players', |
||||||
|
})).toBe('https://instatscout.com/images/players/180/1.png') |
||||||
|
|
||||||
|
expect(getLogo({ |
||||||
|
id: 1, |
||||||
|
sportType: 'basketball', |
||||||
|
type: 'teams', |
||||||
|
})).toBe('https://basketball.instatscout.com/images/teams/180/1.png') |
||||||
|
|
||||||
|
expect(getLogo({ |
||||||
|
id: 1, |
||||||
|
sportType: 'hockey', |
||||||
|
type: 'tournaments', |
||||||
|
})).toBe('https://hockey.instatscout.com/images/tournaments/180/1.png') |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
import type { SportType } from 'features/Search/config' |
||||||
|
|
||||||
|
const IMAGES_URLS = { |
||||||
|
basketball: 'https://basketball.instatscout.com/images', |
||||||
|
football: 'https://instatscout.com/images', |
||||||
|
hockey: 'https://hockey.instatscout.com/images', |
||||||
|
} |
||||||
|
|
||||||
|
type GetLogoArgs = { |
||||||
|
id: number, |
||||||
|
size?: number, |
||||||
|
sportType: SportType, |
||||||
|
type: 'players' | 'teams' | 'tournaments', |
||||||
|
} |
||||||
|
|
||||||
|
export const getLogo = ({ |
||||||
|
id, |
||||||
|
size = 180, |
||||||
|
sportType, |
||||||
|
type, |
||||||
|
}: GetLogoArgs) => ( |
||||||
|
`${IMAGES_URLS[sportType]}/${type}/${size}/${id}.png` |
||||||
|
) |
||||||
@ -1,3 +1,4 @@ |
|||||||
export * from './callApi' |
export * from './callApi' |
||||||
export * from './callApi/getResponseData' |
export * from './callApi/getResponseData' |
||||||
export * from './token' |
export * from './token' |
||||||
|
export * from './getLogo' |
||||||
|
|||||||
@ -0,0 +1,75 @@ |
|||||||
|
import { DATA_URL, PROCEDURES } from 'config' |
||||||
|
import { callApi, getResponseData } from 'helpers' |
||||||
|
|
||||||
|
const proc = PROCEDURES.get_players_teams_tournaments |
||||||
|
|
||||||
|
type Gender = 1 | 2 |
||||||
|
|
||||||
|
type Sport = 1 | 2 | 3 |
||||||
|
|
||||||
|
type Player = { |
||||||
|
firstname_eng: string, |
||||||
|
firstname_rus: string, |
||||||
|
gender?: Gender, |
||||||
|
id: number, |
||||||
|
lastname_eng: string, |
||||||
|
lastname_rus: string, |
||||||
|
sport: Sport, |
||||||
|
team?: { |
||||||
|
id: number, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
type Team = { |
||||||
|
country?: { |
||||||
|
id: number, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
}, |
||||||
|
gender?: Gender, |
||||||
|
id: number, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
sport: Sport, |
||||||
|
} |
||||||
|
|
||||||
|
type Tournament = { |
||||||
|
country?: { |
||||||
|
id: number, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
}, |
||||||
|
gender?: Gender, |
||||||
|
id: number, |
||||||
|
name_eng: string, |
||||||
|
name_rus: string, |
||||||
|
sport: Sport, |
||||||
|
} |
||||||
|
|
||||||
|
export type SearchItems = { |
||||||
|
players?: Array<Player>, |
||||||
|
teams?: Array<Team>, |
||||||
|
tournaments?: Array<Tournament>, |
||||||
|
} |
||||||
|
|
||||||
|
export const getSearchItems = ( |
||||||
|
searchString: string, |
||||||
|
abortSignal: AbortSignal, |
||||||
|
): Promise<SearchItems> => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
params: { |
||||||
|
_p_name: searchString, |
||||||
|
}, |
||||||
|
proc, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
abortSignal, |
||||||
|
config, |
||||||
|
url: DATA_URL, |
||||||
|
}).then(getResponseData(proc)) |
||||||
|
} |
||||||
Loading…
Reference in new issue