parent
18b5c8c2d8
commit
2664368bb1
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -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> |
||||||
|
) |
||||||
@ -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 |
||||||
|
} |
||||||
|
` |
||||||
@ -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…
Reference in new issue