Ott 2144 api select ui

keep-around/2664368bb1711ae1da6d1c28519ce514f90411f7
Макситалиев Мирлан 4 years ago
parent 18b5c8c2d8
commit 2664368bb1
  1. 0
      public/images/settings.svg
  2. 10
      src/config/env.tsx
  3. 12
      src/config/routes.tsx
  4. 28
      src/features/Animation/index.tsx
  5. 5
      src/features/App/AuthenticatedApp.tsx
  6. 4
      src/features/Combobox/hooks/index.tsx
  7. 20
      src/features/HeaderFilters/components/DateFilter/index.tsx
  8. 2
      src/features/HeaderFilters/components/DatePicker/index.tsx
  9. 4
      src/features/HeaderFilters/components/DatePicker/styled.tsx
  10. 2
      src/features/MultiSourcePlayer/components/Settings/styled.tsx
  11. 9
      src/features/OutsideClick/hooks/index.tsx
  12. 2
      src/features/OutsideClick/index.tsx
  13. 70
      src/features/SystemSettings/components/APISettings/index.tsx
  14. 47
      src/features/SystemSettings/components/RadioButtons/index.tsx
  15. 45
      src/features/SystemSettings/components/RadioButtons/stories.tsx
  16. 56
      src/features/SystemSettings/components/RadioButtons/styled.tsx
  17. 47
      src/features/SystemSettings/hooks.tsx
  18. 62
      src/features/SystemSettings/index.tsx
  19. 127
      src/features/SystemSettings/styled.tsx
  20. 2
      src/features/Theme/config.tsx
  21. 14
      src/helpers/selectedApi/index.tsx

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -1,3 +1,13 @@
import includes from 'lodash/includes'
export type ENVType = NodeJS.ProcessEnv['REACT_APP_ENV']
const apis: Array<ENVType> = ['staging', 'preproduction', 'production']
export const isValidEnv = (value: string): value is ENVType => (
Boolean(value) && includes(apis, value)
)
export const ENV = process.env.REACT_APP_ENV || 'staging'
export const isProduction = ENV === 'production' || ENV === 'preproduction'

@ -1,6 +1,8 @@
import { ENV } from './env'
import { readSelectedApi } from 'helpers/selectedApi'
const APIS = {
import { ENV, isProduction } from './env'
export const APIS = {
preproduction: {
api: 'https://api-test.instat.tv',
auth: 'https://auth.instat.tv',
@ -15,6 +17,8 @@ const APIS = {
},
}
export const AUTH_SERVICE = APIS[ENV].auth
export const API_ROOT = APIS[ENV].api
const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const AUTH_SERVICE = APIS[env].auth
export const API_ROOT = APIS[env].api
export const DATA_URL = `${API_ROOT}/data`

@ -0,0 +1,28 @@
import type { ReactNode } from 'react'
import styled, { keyframes } from 'styled-components/macro'
const fadeIn = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`
const Wrapper = styled.div`
opacity: 1;
animation-name: ${fadeIn};
animation-iteration-count: 1;
animation-timing-function: ease-in-out;
animation-duration: 0.3s;
`
type Props = {
children: ReactNode,
className?: string,
}
export const FadeIn = ({ children, className }: Props) => (
<Wrapper className={className}>{children}</Wrapper>
)

@ -7,7 +7,8 @@ import {
} from 'react-router-dom'
import { indexLexics } from 'config/lexics/indexLexics'
import { PAGES } from 'config'
import { isProduction } from 'config/env'
import { PAGES } from 'config/pages'
import { StripeElements } from 'features/StripeElements'
@ -28,6 +29,7 @@ const TeamPage = lazy(() => import('features/TeamPage'))
const MatchPage = lazy(() => import('features/MatchPage'))
const PlayerPage = lazy(() => import('features/PlayerPage'))
const TournamentPage = lazy(() => import('features/TournamentPage'))
const SystemSettings = lazy(() => import('features/SystemSettings'))
export const AuthenticatedApp = () => {
useLexicsConfig(indexLexics)
@ -69,6 +71,7 @@ export const AuthenticatedApp = () => {
</Route>
<Redirect to={PAGES.home} />
</Switch>
{!isProduction && <SystemSettings />}
</NoNetworkPopupStore>
</BuyMatchPopupStore>
</MatchPopupStore>

@ -88,8 +88,8 @@ export const useCombobox = <T extends Option>({
onSelect,
])
const onOutsideClick = (event: MouseEvent) => {
if (event.target !== inputFieldRef.current) {
const onOutsideClick = (event?: MouseEvent) => {
if (event?.target !== inputFieldRef.current) {
close()
}
}

@ -1,7 +1,10 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import { OutsideClick } from 'features/OutsideClick'
import { Date as DateIcon } from 'features/Icons/Date'
import { BodyBackdrop } from 'features/PageLayout'
import { useDateFilter } from './hooks'
import { DatePicker } from '../DatePicker'
@ -73,13 +76,16 @@ export const DateFilter = () => {
</WeekDaysWrapper>
{
isOpen && (
<OutsideClick onClick={close}>
<DatePicker
open
selected={selectedDate}
onChange={onDateChange}
/>
</OutsideClick>
<Fragment>
<OutsideClick onClick={close}>
<DatePicker
open
selected={selectedDate}
onChange={onDateChange}
/>
</OutsideClick>
<BodyBackdrop />
</Fragment>
)
}
</Wrapper>

@ -3,7 +3,6 @@ import DatePickerComponent from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
import { useLexicsStore } from 'features/LexicsStore'
import { BodyBackdrop } from 'features/PageLayout'
import { getDisplayDate } from '../DateFilter/helpers'
import { useDatepickerLocales } from './hooks'
@ -58,7 +57,6 @@ export const DatePicker = ({
useDatepickerLocales()
return (
<Wrapper>
<BodyBackdrop />
<DatePickerComponent
open={open}
selected={selected}

@ -5,12 +5,12 @@ import { BaseButton } from '../DateFilter/styled'
export const Wrapper = styled.div`
position: absolute;
top: calc(2.35rem);
top: 3.5rem;
right: 11rem;
z-index: 10;
${isMobileDevice
? css`
top: 50px;
top: 45px;
right: -10%;
width: 100%;

@ -6,7 +6,7 @@ export const SettingsButton = styled(ButtonBase)`
width: 22px;
height: 20px;
margin-left: 25px;
background-image: url(/images/player-settings.svg);
background-image: url(/images/settings.svg);
${isMobileDevice
? css`

@ -3,7 +3,7 @@ import { useRef } from 'react'
import { useEventListener } from 'hooks'
type Args = {
onClick: (event: MouseEvent) => void,
onClick: (event?: MouseEvent) => void,
}
export const useOutsideClickEffect = ({
@ -21,7 +21,14 @@ export const useOutsideClickEffect = ({
}
}
const onKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClick()
}
}
useEventListener({ callback: handleOutsideClick, event: 'click' })
useEventListener({ callback: onKeyPress, event: 'keydown' })
return wrapperRef
}

@ -8,7 +8,7 @@ type Props = {
/** элемент, которому необходим функционал `OutsideClick` */
children: ReactNode,
/** функция-коллбек, отрабатывающая по клику вне области элемента */
onClick: (event: MouseEvent) => void,
onClick: (event?: MouseEvent) => void,
}
const OutsideClickWrapper = styled.div``

@ -0,0 +1,70 @@
import { Fragment } from 'react'
import styled from 'styled-components/macro'
import map from 'lodash/map'
import type { ENVType } from 'config/env'
import { APIS } from 'config/routes'
import { RadioGroup, RadioButton } from '../RadioButtons'
import { SettingsDescription } from '../../styled'
const Details = styled.span`
margin-top: 4px;
font-size: 10px;
text-transform: initial;
`
const getHost = (url: string) => new URL(url).host
type Option = {
details: string,
key: ENVType,
label: string,
}
const options: Array<Option> = [
{
details: getHost(APIS.staging.api),
key: 'staging',
label: 'Стейджинг',
},
{
details: getHost(APIS.preproduction.api),
key: 'preproduction',
label: 'Тест',
},
{
details: getHost(APIS.production.api),
key: 'production',
label: 'Продакшн',
},
]
type Props = {
onChange?: (value: ENVType) => void,
selectedApi: ENVType,
}
export const APISettings = ({ onChange, selectedApi }: Props) => (
<Fragment>
<SettingsDescription>Подключенный API</SettingsDescription>
<RadioGroup>
{
map(options, (option) => (
<RadioButton
key={option.key}
name='api'
onChange={onChange}
value={option.key}
defaultChecked={option.key === selectedApi}
>
{option.label}
<Details>{option.details}</Details>
</RadioButton>
))
}
</RadioGroup>
</Fragment>
)

@ -0,0 +1,47 @@
import React, { ReactNode } from 'react'
import {
Group,
RadioWrapper,
Label,
Input,
} from './styled'
type Props<T> = {
children: ReactNode,
defaultChecked?: boolean,
name?: string,
onChange?: (value: T) => void,
value: T,
}
export const RadioButton = <T extends string>({
children,
defaultChecked,
name,
onChange,
value,
}: Props<T>) => (
<RadioWrapper>
<Label htmlFor={`${name}_${value}`}>
{children}
</Label>
<Input
id={`${name}_${value}`}
name={name}
value={value}
defaultChecked={defaultChecked}
onChange={(e) => onChange?.(e.target.value as T)}
/>
</RadioWrapper>
)
type RadioGroupProps = {
children: ReactNode,
}
export const RadioGroup = ({ children }: RadioGroupProps) => (
<Group>
{children}
</Group>
)

@ -0,0 +1,45 @@
import { useState } from 'react'
import { RadioGroup, RadioButton } from '.'
const Story = {
component: RadioGroup,
title: 'RadioButtons',
}
export default Story
export const TwoButtons = () => {
const [selected, setSelected] = useState('1')
return (
<div style={{ width: 400 }}>
<RadioGroup>
<RadioButton
defaultChecked={selected === '0'}
name='api'
value='0'
onChange={setSelected}
>
Staging
</RadioButton>
<RadioButton
defaultChecked={selected === '1'}
name='api'
value='1'
onChange={setSelected}
>
Test
</RadioButton>
<RadioButton
defaultChecked={selected === '2'}
name='api'
value='2'
onChange={setSelected}
>
Production
</RadioButton>
</RadioGroup>
</div>
)
}

@ -0,0 +1,56 @@
import styled from 'styled-components/macro'
export const Group = styled.div`
padding: 6px;
display: flex;
border-radius: 6px;
background-color: #1C1C1E;
`
type WrapperProps = {
checked?: boolean,
}
export const RadioWrapper = styled.div<WrapperProps>`
position: relative;
flex: 1;
font-size: 12px;
border-radius: inherit;
color: white;
display: flex;
align-items: center;
`
export const Label = styled.label`
display: flex;
flex-direction: column;
z-index: 1;
width: 100%;
padding: 8px;
cursor: pointer;
text-transform: uppercase;
`
export const Input = styled.input.attrs(
() => ({
type: 'radio',
}),
)`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
appearance: none;
margin: 0;
transition: background-color 0.3s;
border-radius: inherit;
:checked {
background-color: #45464b;
}
:hover {
background-color: #2c2c30;
}
`

@ -0,0 +1,47 @@
import type { FormEvent } from 'react'
import {
ENV,
ENVType,
isValidEnv,
} from 'config/env'
import { SELECTED_API_KEY } from 'helpers/selectedApi'
import { useToggle } from 'hooks/useToggle'
import { useLocalStore } from 'hooks/useStorage'
type FormElement = HTMLFormElement & {
api: HTMLInputElement & {
value: ENVType,
},
}
export const useSystemSettings = () => {
const {
close,
isOpen,
open,
} = useToggle()
const [selectedApi, setSelectedApi] = useLocalStore({
defaultValue: ENV,
key: SELECTED_API_KEY,
validator: isValidEnv,
})
const onSubmit = (e: FormEvent<FormElement>) => {
e.preventDefault()
const { api } = e.currentTarget
setSelectedApi(api.value)
window.location.reload()
}
return {
close,
isOpen,
onSubmit,
open,
selectedApi,
}
}

@ -0,0 +1,62 @@
import { Fragment } from 'react'
import { OutsideClick } from 'features/OutsideClick'
import { APISettings } from './components/APISettings'
import { useSystemSettings } from './hooks'
import {
FadeIn,
Form,
Content,
Footer,
Header,
HeaderTitle,
TriggerButton,
ApplyButton,
CloseButton,
} from './styled'
const SystemSettings = () => {
const {
close,
isOpen,
onSubmit,
open,
selectedApi,
} = useSystemSettings()
return (
<Fragment>
<TriggerButton aria-label='Settings' onClick={open} />
{
isOpen && (
<FadeIn>
<OutsideClick onClick={close}>
<Form onSubmit={onSubmit}>
<Header>
<HeaderTitle>Настройки</HeaderTitle>
<CloseButton
aria-label='Close'
type='button'
onClick={close}
>
X
</CloseButton>
</Header>
<Content>
<APISettings selectedApi={selectedApi} />
</Content>
<Footer>
<ApplyButton>Применить</ApplyButton>
</Footer>
</Form>
</OutsideClick>
</FadeIn>
)
}
</Fragment>
)
}
export default SystemSettings

@ -0,0 +1,127 @@
import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { FadeIn as FadeInBase } from 'features/Animation'
export const FadeIn = styled(FadeInBase)`
z-index: 1000000;
`
export const Form = styled.form`
--spacing-medium: 12px;
--spacing-large: 20px;
--radius: 8px;
position: fixed;
bottom: 0;
right: 0;
width: 390px;
max-height: 500px;
border-radius: var(--radius);
box-shadow: 0 7px 10px 2px rgb(0, 0, 0);
margin: var(--spacing-large);
padding: var(--spacing-medium);
background-color: #333333;
color: white;
text-align: center;
@media ${devices.mobile} {
--spacing-large: 12px;
width: calc(100% - var(--spacing-large) * 2);
}
`
export const Header = styled.div`
position: relative;
`
export const HeaderTitle = styled.h3`
font-size: 20px;
`
export const Content = styled.div`
width: 100%;
height: 100%;
margin-top: var(--spacing-medium);
display: flex;
flex-direction: column;
`
export const SettingsDescription = styled.p`
padding: 8px;
font-size: 14px;
text-align: left;
`
export const Footer = styled.div`
margin-top: var(--spacing-medium);
`
const BaseButton = styled.button`
appearance: none;
border: none;
background: none;
padding: 0;
cursor: pointer;
`
export const TriggerButton = styled(BaseButton)`
position: fixed;
z-index: 10;
bottom: 0;
right: 150px;
margin: 14px;
width: 46px;
height: 46px;
border-radius: 50%;
background-color: #333333;
background-image: url(/images/settings.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 16px;
transition: background-color 0.3s;
box-shadow: 0px 3px 5px 1px rgb(0 0 0);
:hover {
background-color: #45464b;
}
@media ${devices.mobile} {
right: 70px;
margin: 12px;
}
`
export const ApplyButton = styled(BaseButton)`
width: 100%;
padding: var(--spacing-medium);
font-size: 16px;
color: white;
border-radius: var(--radius);
background-color: ${({ theme }) => theme.colors.button};
:hover {
background-color: ${({ theme }) => theme.colors.buttonHover};
}
`
export const CloseButton = styled(BaseButton)`
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
width: 24px;
height: 24px;
font-size: 10px;
color: white;
background-color: #45464b;
transition: background-color 0.3s;
border-radius: 50%;
:hover {
background-color: #595959
}
`

@ -5,6 +5,7 @@ export const lightTheme = {
black40: '',
black70: '',
button: '',
buttonHover: '',
error: '',
inputs: '',
primary: '',
@ -32,6 +33,7 @@ export const darkTheme = {
black40: 'rgba(0, 0, 0, 0.4)',
black70: 'rgba(0, 0, 0, 0.7)',
button: '#294FC3',
buttonHover: '#3255be',
error: 'rgba(235, 87, 87, 1)',
inputs: '#3F3F3F',
primary: `

@ -0,0 +1,14 @@
import { isValidEnv } from 'config/env'
export const SELECTED_API_KEY = 'selected_api'
export const readSelectedApi = () => {
const rawValue = localStorage.getItem(SELECTED_API_KEY)
if (!rawValue) return null
const selectedApi = JSON.parse(rawValue)
if (selectedApi && isValidEnv(selectedApi)) {
return selectedApi
}
return null
}
Loading…
Cancel
Save