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