Ott 102 search (#40)

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Ruslan Khayrullin 5 years ago committed by GitHub
parent 5bfa0d7b7e
commit c23e650512
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      public/images/player.svg
  2. 4
      public/images/search.svg
  3. 8
      public/images/team.svg
  4. 3
      public/images/tournament.svg
  5. 5
      src/config/colors.tsx
  6. 1
      src/config/index.tsx
  7. 6
      src/config/lexics/authenticated.tsx
  8. 1
      src/config/procedures.tsx
  9. 20
      src/config/routes.tsx
  10. 14
      src/features/Combobox/styled.tsx
  11. 3
      src/features/Common/Input/index.tsx
  12. 1
      src/features/Common/index.tsx
  13. 22
      src/features/Common/сustomScrollbar/index.tsx
  14. 62
      src/features/ItemsList/hooks.tsx
  15. 72
      src/features/ItemsList/index.tsx
  16. 67
      src/features/ItemsList/styled.tsx
  17. 21
      src/features/Search/components/Header/index.tsx
  18. 24
      src/features/Search/components/Header/styled.tsx
  19. 11
      src/features/Search/config/index.tsx
  20. 85
      src/features/Search/hooks/index.tsx
  21. 104
      src/features/Search/hooks/useNormalizedItems.tsx
  22. 70
      src/features/Search/index.tsx
  23. 63
      src/features/Search/styled.tsx
  24. 7
      src/features/T9n/index.tsx
  25. 23
      src/helpers/getLogo/__tests__/index.tsx
  26. 23
      src/helpers/getLogo/index.tsx
  27. 1
      src/helpers/index.tsx
  28. 75
      src/requests/getSearchItems.tsx
  29. 1
      src/requests/index.tsx

@ -0,0 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.60389 17.9999C4.59801 17.9999 5.40389 17.194 5.40389 16.1999C5.40389 15.2058 4.59801 14.3999 3.60389 14.3999C2.60978 14.3999 1.80389 15.2058 1.80389 16.1999C1.80389 17.194 2.60978 17.9999 3.60389 17.9999Z" fill="white"/>
<path d="M10.8039 3.6C11.798 3.6 12.6039 2.79411 12.6039 1.8C12.6039 0.805887 11.798 0 10.8039 0C9.80979 0 9.00391 0.805887 9.00391 1.8C9.00391 2.79411 9.80979 3.6 10.8039 3.6Z" fill="white"/>
<path d="M17.5989 7.37996L13.4139 5.12996C12.7389 4.76996 12.1989 4.49996 11.4339 4.40996L9.90386 4.09496C9.45386 4.00496 9.00386 3.95996 8.55386 4.00496C8.46386 4.00496 8.41886 4.00496 8.32886 4.04996C7.78886 4.13996 7.29386 4.36496 6.79886 4.63496C5.76386 5.26496 4.99886 6.20996 4.45886 7.33496L4.00886 8.32496C3.82886 8.72996 4.00886 9.17996 4.41386 9.35996C4.50386 9.40496 4.63886 9.44996 4.72886 9.44996C5.04386 9.44996 5.31386 9.26996 5.44886 8.99996L5.89886 8.00996C6.30386 7.15496 6.84386 6.47996 7.65386 5.98496C7.65386 5.98496 7.69886 5.93996 7.74386 5.93996L7.69886 6.11996L6.12386 10.17L10.3989 11.295L12.2889 6.43496V6.34496C12.4239 6.38996 12.5589 6.47996 12.7389 6.56996L16.8789 8.81996C17.2839 9.04496 17.7339 8.86496 17.9589 8.50496C18.0939 8.09996 17.9589 7.60496 17.5989 7.37996Z" fill="white"/>
<path d="M10.1739 9.54004L7.2489 17.19" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.78892 8.18994L5.76392 12.7799" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,4 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10.5" cy="10.5" r="9" stroke="#999999" stroke-width="3"/>
<rect x="18.0607" y="15.9393" width="9.01471" height="3" rx="1.5" transform="rotate(45 18.0607 15.9393)" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 295 B

@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.204 17.0067C12.1978 16.8454 12.8727 15.9091 12.7114 14.9153C12.5501 13.9215 11.6137 13.2466 10.62 13.4079C9.62618 13.5692 8.9513 14.5056 9.11259 15.4994C9.27387 16.4931 10.2102 17.168 11.204 17.0067Z" fill="white"/>
<path d="M13.2069 6.54764C13.1697 5.05954 11.942 3.86906 10.4539 3.86906H9.44946C10.2679 3.64585 10.8632 2.86459 10.8632 1.97173C10.8632 0.89286 9.97029 0 8.89142 0C7.81255 0 6.91969 0.89286 6.91969 1.97173C6.91969 2.86459 7.51493 3.60864 8.33338 3.86906H7.32891C5.84081 3.86906 4.65033 5.05954 4.57593 6.54764V6.58485V10.0819C4.57593 10.4539 4.87355 10.7515 5.24557 10.7515C5.6176 10.7515 5.91522 10.4539 5.91522 10.0819V6.58485H6.25004V15.9971C6.25004 16.6295 6.77088 17.1504 7.40332 17.1504C8.03576 17.1504 8.5566 16.6295 8.5566 15.9971V10.4539H9.22624V12.1652C9.22624 12.7977 9.74708 13.3185 10.3795 13.3185C11.012 13.3185 11.5328 12.7977 11.5328 12.1652V10.4539V6.54764H11.8676V10.0447C11.8676 10.4167 12.1652 10.7143 12.5373 10.7143C12.9093 10.7143 13.2069 10.4167 13.2069 10.0447V6.54764Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.32445 1.97173C6.32445 3.05061 5.43159 3.94347 4.35271 3.94347C3.27384 3.94347 2.38098 3.05061 2.38098 1.97173C2.38098 0.89286 3.27384 0 4.35271 0C5.43159 0 6.32445 0.89286 6.32445 1.97173ZM5.58042 1.97162C5.58042 1.30197 5.02239 0.743933 4.35274 0.743933C3.6831 0.743933 3.16226 1.30197 3.12506 1.97162C3.12506 2.64126 3.6831 3.1993 4.35274 3.1993C5.02239 3.1993 5.58042 2.64126 5.58042 1.97162Z" fill="white"/>
<path d="M3.98067 15.6252C3.98067 16.0716 3.60864 16.4436 3.16221 16.4436H3.12501C2.67858 16.4436 2.30656 16.0716 2.30656 15.6252V9.63559V9.59838V6.62218H1.63691V9.56118V9.63559C1.63691 9.89601 1.4137 10.1192 1.15328 10.1192C0.89286 10.1192 0.669645 9.89601 0.669645 9.63559V9.26356V6.73379C0.669645 5.54331 1.63691 4.61325 2.79019 4.61325H4.65031C4.87353 4.35283 5.17115 4.12962 5.46877 3.9436H4.57591H3.98067H2.79019C1.30209 3.9436 0.074405 5.13408 0 6.62218V9.59838C0 10.2308 0.520835 10.7517 1.15328 10.7517C1.33929 10.7517 1.4881 10.7145 1.63691 10.6401V15.6252C1.63691 16.4436 2.30656 17.0761 3.08781 17.0761H3.12501C3.94347 17.0761 4.57591 16.4064 4.57591 15.6252V11.0493H3.90626V15.6252H3.98067Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4762 1.97173C15.4762 3.05061 14.5834 3.94347 13.5045 3.94347C12.4256 3.94347 11.5328 3.05061 11.5328 1.97173C11.5328 0.89286 12.4256 0 13.5045 0C14.5834 0 15.4762 0.89286 15.4762 1.97173ZM14.7322 1.97162C14.7322 1.30197 14.1742 0.743933 13.5045 0.743933C12.8349 0.743933 12.2768 1.30197 12.2768 1.97162C12.2768 2.64126 12.8349 3.1993 13.5045 3.1993C14.1742 3.1993 14.7322 2.64126 14.7322 1.97162Z" fill="white"/>
<path d="M17.82 6.62218C17.7828 5.13408 16.5551 3.9436 15.0298 3.9436H13.8394H13.2441H12.3513C12.6489 4.12962 12.9465 4.35283 13.1697 4.61325H15.0298C16.2203 4.61325 17.1504 5.58051 17.1504 6.73379V9.22636V9.59839C17.1504 9.8588 16.9272 10.082 16.6667 10.082C16.4063 10.082 16.1831 9.8588 16.1831 9.59839V9.52398V6.58498H15.5135V9.56118V9.59839V15.588C15.5135 16.0344 15.1414 16.4064 14.695 16.4064H14.6578C14.2114 16.4064 13.8394 16.0344 13.8394 15.588V11.0121H13.1697V15.588C13.1697 16.4064 13.8394 17.0389 14.6206 17.0389H14.6578C15.4763 17.0389 16.1087 16.3692 16.1087 15.588V10.6029C16.2575 10.6773 16.4063 10.7145 16.5923 10.7145C17.2248 10.7145 17.7456 10.1936 17.7456 9.56118V9.18916V6.73379L17.82 6.62218Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.75 1.66675H5.25003C5.04007 1.66675 4.83871 1.75016 4.69024 1.89862C4.54177 2.04709 4.45837 2.24845 4.45837 2.45841V3.52951H3.14451C2.73994 3.53014 2.35227 3.61657 2.06619 3.76992C1.78011 3.92327 1.61888 4.13107 1.61771 4.34794C1.60989 5.23557 2.08185 6.10542 2.97656 6.85234C3.49918 7.30563 4.16759 7.70476 4.94951 8.03156L4.95316 8.04038L4.95538 8.04481L4.9587 8.05122C4.97165 8.0818 4.98659 8.1115 5.00343 8.14013C5.38886 9.02315 5.96626 9.80916 6.69365 10.441C7.42103 11.0728 8.2801 11.5346 9.20837 11.7926V13.844L6.47925 15.2086C6.34776 15.2744 6.23717 15.3754 6.15989 15.5005C6.08261 15.6256 6.04168 15.7697 6.0417 15.9167V16.7084C6.0417 16.9184 6.12511 17.1197 6.27357 17.2682C6.42204 17.4167 6.6234 17.5001 6.83337 17.5001H13.1667C13.3767 17.5001 13.578 17.4167 13.7265 17.2682C13.875 17.1197 13.9584 16.9184 13.9584 16.7084V15.9167C13.9584 15.7697 13.9175 15.6256 13.8402 15.5005C13.7629 15.3754 13.6523 15.2744 13.5208 15.2086L10.7917 13.844V11.7926C11.7196 11.5347 12.5783 11.0732 13.3055 10.4418C14.0327 9.81047 14.6101 9.02503 14.9958 8.14258C15.0153 8.10985 15.0324 8.07575 15.0469 8.04053C15.0487 8.03616 15.0505 8.03179 15.0523 8.02741C16.0207 7.61504 16.8177 7.09763 17.3896 6.50814C18.0376 5.84021 18.378 5.09908 18.3823 4.34651C18.3811 4.13002 18.2199 3.92258 17.9338 3.7695C17.6477 3.61642 17.26 3.53014 16.8554 3.52951H15.5417V2.45841C15.5417 2.24845 15.4583 2.04709 15.3098 1.89862C15.1614 1.75016 14.96 1.66675 14.75 1.66675Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,5 @@
export const SPORT_COLORS = {
basketball: '#f1903b',
football: '#00a435',
hockey: '#5eb1ff',
}

@ -2,3 +2,4 @@ export * from './routes'
export * from './pages'
export * from './authKeys'
export * from './procedures'
export * from './colors'

@ -1,4 +1,10 @@
export const authenticatedLexics = {
basketball: 6960,
football: 6958,
hockey: 6959,
logout: 4306,
player: 630,
team: 658,
tournament: 1009,
user_account: 12928,
}

@ -2,6 +2,7 @@ export const PROCEDURES = {
auth_user: 'auth_user',
create_user: 'create_user',
get_cities: 'get_cities',
get_players_teams_tournaments: 'get_players_teams_tournaments',
lst_c_country: 'lst_c_country',
param_lexical: 'param_lexical',
}

@ -1,2 +1,22 @@
export const API_ROOT = 'http://api-staging.instat.tv'
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',
},
}

@ -9,6 +9,7 @@ import {
} from '@reach/combobox'
import { wrapperStyles, inputStyles } from 'features/Common/Input/styled'
import { сustomScrollbar } from 'features/Common'
export const ComboboxStyled = styled(Combobox)`
${wrapperStyles}
@ -53,18 +54,7 @@ export const ComboboxListStyled = styled(ComboboxList)`
left: -164px;
overflow: auto;
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #3F3F3F;
border-radius: 6px;
}
${сustomScrollbar}
`
export const ComboboxOptionStyled = styled(ComboboxOption)`

@ -22,6 +22,7 @@ type Props = {
maxLength?: number,
onBlur?: (event: FocusEvent<HTMLInputElement>) => void,
onChange?: (event: ChangeEvent<HTMLInputElement>) => void,
onFocus?: (event: FocusEvent<HTMLInputElement>) => void,
pattern?: string,
required?: boolean,
title?: string,
@ -40,6 +41,7 @@ export const Input = ({
maxLength,
onBlur,
onChange,
onFocus,
paddingX,
pattern,
required,
@ -72,6 +74,7 @@ export const Input = ({
defaultValue={defaultValue}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
maxLength={maxLength}
inputWidth={inputWidth}
pattern={pattern}

@ -3,3 +3,4 @@ export * from './Button'
export * from './Radio'
export * from './Checkbox'
export * from './Arrows'
export * from './сustomScrollbar'

@ -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'
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}
`

@ -9,22 +9,25 @@ const Text = styled.span<TCustomStyles>`
${({ customStyles }) => customStyles}
`
type TT9n = {
type T9nProps = {
className?: string,
onClick?: () => void,
t: string | number,
} & TCustomStyles
export const T9n = ({
className,
customStyles,
onClick,
t,
}: TT9n) => {
}: T9nProps) => {
const { translate } = useLexicsStore()
return (
<Text
onClick={onClick}
customStyles={customStyles}
className={className}
>
{translate(String(t))}
</Text>

@ -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/getResponseData'
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))
}

@ -3,3 +3,4 @@ export * from './login'
export * from './getCountries'
export * from './getCountryCities'
export * from './getLexics'
export * from './getSearchItems'

Loading…
Cancel
Save