Ott 1725 auth service (#575)
* Ott 1725 part 1 (#528) * chore(1725): configured auth service app * feat(1725): new LanguageSelect component * feat(1725): restored old components * Ott 1725 part 2 (#545) * feat(1725): login page * feat(1725): registration page * fix(1725): review fix * Ott 1725 part 3 (#547) * fix(1725): refreshing token * fix(1725): added logo and background client configs * fix(1725): sync lang change (#548) * Ott 1725 part 5 (#549) * fix(1725): setting icons, title, desc in head tag * fix(1725): added lexics * fix(1725): facr input and button customizations * Ott 1725 part 6 (#560) * fix(1725): scale on mobile * refactor(1725): fixed scrollbar issue when refreshing token * refactor(1725): removed token refreshing * fix(1725): fix build command and redirecting * refactor(1725): login check and final touches * Ott 1956 register popup (#574) * Ott 1725 part 1 (#528) * chore(1725): configured auth service app * feat(1725): new LanguageSelect component * feat(1725): restored old components * feat(#1956): add popup on register page * refactor(#1956): fix makefile * feat(#1956): add popup on register page * refactor(#1956): fix makefile * refactor(#1956): fix comments * refactor(#1956): fix comments * Update index.tsx Co-authored-by: Mirlan <m.maksitaliev@gmail.com> Co-authored-by: Andrei Dekterev <57942757+dekterev@users.noreply.github.com>keep-around/100b3f5493bc24e9889c376dcfc797564582c41c
parent
1b9f0f8251
commit
f692b42d82
@ -1,2 +0,0 @@ |
||||
const title = 'FACR.TV - The home of Czech football streaming' |
||||
const desc = 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices' |
||||
@ -1,2 +0,0 @@ |
||||
const title = 'InStat TV - The Home of Sports Streaming' |
||||
const desc = 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.' |
||||
|
After Width: | Height: | Size: 14 KiB |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,51 @@ |
||||
import { lazy } from 'react' |
||||
|
||||
import { |
||||
Redirect, |
||||
Route, |
||||
Switch, |
||||
} from 'react-router-dom' |
||||
|
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { useLexicsConfig } from 'features/LexicsStore' |
||||
|
||||
import { PAGES } from '../../config/pages' |
||||
import { lexics } from '../../config/lexics' |
||||
|
||||
const Login = lazy(() => import('../Login')) |
||||
const Registration = lazy(() => import('../Registration')) |
||||
|
||||
const Main = styled.main` |
||||
width: 100%; |
||||
${isMobileDevice |
||||
? css` |
||||
padding: 0 12px; |
||||
@media screen and (orientation: landscape){ |
||||
min-height: 100vh; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const App = () => { |
||||
useLexicsConfig(lexics) |
||||
|
||||
return ( |
||||
<Main> |
||||
<Switch> |
||||
<Route path={PAGES.login}> |
||||
<Login /> |
||||
</Route> |
||||
|
||||
<Route path={PAGES.registration}> |
||||
<Registration /> |
||||
</Route> |
||||
|
||||
<Redirect to={PAGES.login} /> |
||||
</Switch> |
||||
</Main> |
||||
) |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
import { useMemo } from 'react' |
||||
|
||||
import find from 'lodash/find' |
||||
|
||||
import { langsList, Languages } from 'config/languages' |
||||
import { useToggle } from 'hooks/useToggle' |
||||
|
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
|
||||
export const useLanguageSelect = () => { |
||||
const { changeLang, lang } = useLexicsStore() |
||||
const { |
||||
close, |
||||
isOpen, |
||||
open, |
||||
} = useToggle() |
||||
|
||||
const handleLangChange = (locale: Languages) => () => { |
||||
changeLang(locale) |
||||
close() |
||||
} |
||||
|
||||
const selectedLang = useMemo(() => find(langsList, { locale: lang }), [lang]) |
||||
|
||||
return { |
||||
close, |
||||
handleLangChange, |
||||
isOpen, |
||||
open, |
||||
selectedLang, |
||||
} |
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
import map from 'lodash/map' |
||||
|
||||
import { langsList } from 'config/languages' |
||||
|
||||
import { OutsideClick } from 'features/OutsideClick' |
||||
|
||||
import { useLanguageSelect } from './hooks' |
||||
import { |
||||
Wrapper, |
||||
Button, |
||||
ButtonTitle, |
||||
ListWrapper, |
||||
LangsList, |
||||
LangsItem, |
||||
FlagIcon, |
||||
LangName, |
||||
} from './styled' |
||||
|
||||
export const LanguageSelect = () => { |
||||
const { |
||||
close, |
||||
handleLangChange, |
||||
isOpen, |
||||
open, |
||||
selectedLang, |
||||
} = useLanguageSelect() |
||||
|
||||
return ( |
||||
<Wrapper> |
||||
{selectedLang && ( |
||||
<Button |
||||
type='button' |
||||
aria-controls='langsList' |
||||
aria-expanded={isOpen} |
||||
onClick={open} |
||||
> |
||||
<FlagIcon flag={selectedLang.className} /> |
||||
<ButtonTitle>{selectedLang.title}</ButtonTitle> |
||||
</Button> |
||||
)} |
||||
{isOpen && ( |
||||
<OutsideClick onClick={close}> |
||||
<ListWrapper> |
||||
<LangsList id='langsList'> |
||||
{ |
||||
map( |
||||
langsList, |
||||
({ |
||||
className, |
||||
locale, |
||||
title, |
||||
}) => ( |
||||
<LangsItem |
||||
key={locale} |
||||
selected={selectedLang?.locale === locale} |
||||
onClick={handleLangChange(locale)} |
||||
> |
||||
<FlagIcon flag={className} /> |
||||
<LangName>{title}</LangName> |
||||
</LangsItem> |
||||
), |
||||
) |
||||
} |
||||
</LangsList> |
||||
</ListWrapper> |
||||
</OutsideClick> |
||||
)} |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,147 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { langsList } from 'config/languages' |
||||
import { customScrollbar } from 'features/Common/customScrollbar' |
||||
|
||||
export const Wrapper = styled.div` |
||||
position: relative; |
||||
display: flex; |
||||
align-items: center; |
||||
` |
||||
|
||||
export const Button = styled.button` |
||||
padding: 0; |
||||
border: none; |
||||
background: none; |
||||
cursor: pointer; |
||||
|
||||
display: flex; |
||||
align-items: center; |
||||
width: 192px; |
||||
height: 44px; |
||||
padding-left: 20px; |
||||
|
||||
::after { |
||||
content: ''; |
||||
position: absolute; |
||||
right: 2px; |
||||
bottom: 50%; |
||||
transform: rotate(45deg); |
||||
width: 9px; |
||||
height: 9px; |
||||
border-right: 2px solid; |
||||
border-bottom: 2px solid; |
||||
border-color: ${({ theme }) => theme.colors.text100}; |
||||
} |
||||
` |
||||
|
||||
export const ButtonTitle = styled.span` |
||||
padding-left: 10px; |
||||
font-size: 16px; |
||||
font-weight: normal; |
||||
letter-spacing: -0.32px; |
||||
color: ${({ theme }) => theme.colors.text100}; |
||||
` |
||||
|
||||
export const ListWrapper = styled.div` |
||||
width: 192px; |
||||
height: 220px; |
||||
z-index: 1; |
||||
position: absolute; |
||||
top: calc(100% - 4px); |
||||
left: 0; |
||||
padding: 10px; |
||||
background-color: #333333; |
||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); |
||||
border-radius: 2px; |
||||
` |
||||
|
||||
export const LangsList = styled.ul` |
||||
display: flex; |
||||
height: 100%; |
||||
flex-wrap: wrap; |
||||
overflow-y: auto; |
||||
${customScrollbar} |
||||
|
||||
::-webkit-scrollbar { |
||||
width: 5px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb { |
||||
background: rgba(255, 255, 255, 0.2); |
||||
} |
||||
` |
||||
|
||||
type LangsItemProps = { |
||||
selected: boolean, |
||||
} |
||||
|
||||
export const LangsItem = styled.li<LangsItemProps>` |
||||
padding-left: 10px; |
||||
margin-right: 10px; |
||||
text-align: center; |
||||
display: flex; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 34px; |
||||
border-radius: 2px; |
||||
transition: 0.3s; |
||||
font-weight: normal; |
||||
font-size: 16px; |
||||
color: ${({ theme }) => theme.colors.text70}; |
||||
:hover { |
||||
background-color: #99999940; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
${({ selected, theme }) => ( |
||||
selected |
||||
? css` |
||||
font-weight: 600; |
||||
color: ${theme.colors.text100}; |
||||
` |
||||
: '' |
||||
)} |
||||
` |
||||
|
||||
export const LangName = styled.span` |
||||
padding-left: 10px; |
||||
` |
||||
|
||||
type LanguageFlags = typeof langsList[number]['className'] |
||||
type Position = { |
||||
col: number, |
||||
row: number, |
||||
} |
||||
|
||||
const flagPositions: Record<LanguageFlags, Position> = { |
||||
cz: { col: 1, row: 3 }, |
||||
de: { col: 5, row: 4 }, |
||||
es: { col: 12, row: 10 }, |
||||
fr: { col: 1, row: 4 }, |
||||
gb: { col: 4, row: 12 }, |
||||
it: { col: 8, row: 5 }, |
||||
pl: { col: 4, row: 9 }, |
||||
pt: { col: 5, row: 9 }, |
||||
ru: { col: 8, row: 9 }, |
||||
ua: { col: 2, row: 12 }, |
||||
} |
||||
|
||||
const getFlagPosition = (flag: LanguageFlags) => { |
||||
const { col, row } = flagPositions[flag] |
||||
return `${col * -24}px ${row * -16}px` |
||||
} |
||||
|
||||
type FlagIconProps = { |
||||
flag: LanguageFlags, |
||||
} |
||||
|
||||
export const FlagIcon = styled.span<FlagIconProps>` |
||||
display: inline-block; |
||||
width: 20px; |
||||
height: 14px; |
||||
background-image: url('/images/flags-sprite.png'); |
||||
background-repeat: no-repeat; |
||||
background-size: 360px; |
||||
background-position: ${({ flag }) => getFlagPosition(flag)}; |
||||
` |
||||
@ -0,0 +1,75 @@ |
||||
import type { FormEvent } from 'react' |
||||
import { |
||||
useRef, |
||||
useState, |
||||
useEffect, |
||||
} from 'react' |
||||
|
||||
import { addLanguageUrlParam } from 'helpers/languageUrlParam' |
||||
|
||||
import { loginCheck } from 'features/AuthServiceApp/requests/auth' |
||||
import { getApiUrl } from 'features/AuthServiceApp/config/routes' |
||||
|
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields' |
||||
|
||||
const url = getApiUrl('/authorize') |
||||
|
||||
export const useLoginForm = () => { |
||||
const { lang } = useLexicsStore() |
||||
const [authError, setAuthError] = useState('') |
||||
const [isFetching, setIsFetching] = useState(false) |
||||
const formRef = useRef<HTMLFormElement>(null) |
||||
const { |
||||
email, |
||||
error: formError, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
password, |
||||
} = useAuthFields() |
||||
|
||||
const isSubmitDisabled = ( |
||||
!email |
||||
|| !password |
||||
|| Boolean(formError) |
||||
|| Boolean(authError) |
||||
|| isFetching |
||||
) |
||||
|
||||
const submitForm = () => formRef.current?.submit() |
||||
|
||||
const handleError = (error: string) => { |
||||
setAuthError(error) |
||||
setIsFetching(false) |
||||
} |
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { |
||||
e.preventDefault() |
||||
setIsFetching(true) |
||||
loginCheck(email, password) |
||||
.then(submitForm) |
||||
.catch(handleError) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
setAuthError('') |
||||
}, [email, password]) |
||||
|
||||
return { |
||||
authError, |
||||
email, |
||||
formError, |
||||
formRef, |
||||
handleSubmit, |
||||
isFetching, |
||||
isSubmitDisabled, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
password, |
||||
url: addLanguageUrlParam(lang, url), |
||||
} |
||||
} |
||||
@ -0,0 +1,96 @@ |
||||
import { T9n } from 'features/T9n' |
||||
import { ArrowLoader } from 'features/ArrowLoader' |
||||
|
||||
import { PAGES } from '../../config/pages' |
||||
import { LanguageSelect } from '../LanguageSelect' |
||||
import { PasswordInput } from '../PasswordInput' |
||||
import { Input } from '../Input' |
||||
import { Logo } from '../Logo' |
||||
import { useLoginForm } from './hooks' |
||||
import { |
||||
InputGroup, |
||||
BlockTitle, |
||||
CenterBlock, |
||||
ButtonsBlock, |
||||
Form, |
||||
ButtonSolid, |
||||
Error, |
||||
LanguageSelectWrapper, |
||||
} from '../../styled' |
||||
import { RegisterButton } from './styled' |
||||
|
||||
const Login = () => { |
||||
const { |
||||
authError, |
||||
email, |
||||
formError, |
||||
formRef, |
||||
handleSubmit, |
||||
isFetching, |
||||
isSubmitDisabled, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
password, |
||||
url, |
||||
} = useLoginForm() |
||||
|
||||
return ( |
||||
<CenterBlock> |
||||
<Logo /> |
||||
<Form |
||||
method='POST' |
||||
ref={formRef} |
||||
action={url} |
||||
onSubmit={handleSubmit} |
||||
> |
||||
<BlockTitle t='step_title_login' /> |
||||
|
||||
<InputGroup> |
||||
<Input |
||||
autoFocus |
||||
type='email' |
||||
name='email' |
||||
autoComplete='email' |
||||
placeholderLexic='form_email' |
||||
value={email} |
||||
onChange={onEmailChange} |
||||
onBlur={onEmailBlur} |
||||
/> |
||||
<PasswordInput |
||||
type='password' |
||||
name='password' |
||||
autoComplete='current-password' |
||||
placeholderLexic='form_password' |
||||
value={password} |
||||
onChange={onPasswordChange} |
||||
onBlur={onPasswordBlur} |
||||
/> |
||||
</InputGroup> |
||||
|
||||
<ButtonsBlock> |
||||
<Error> |
||||
<T9n t={formError} /> |
||||
<T9n t={authError} /> |
||||
</Error> |
||||
<ButtonSolid type='submit' disabled={isSubmitDisabled}> |
||||
{ |
||||
isFetching |
||||
? <ArrowLoader /> |
||||
: <T9n t='login' /> |
||||
} |
||||
</ButtonSolid> |
||||
<RegisterButton to={`${PAGES.registration}${window.location.search}`}> |
||||
<T9n t='register' /> |
||||
</RegisterButton> |
||||
</ButtonsBlock> |
||||
<LanguageSelectWrapper> |
||||
<LanguageSelect /> |
||||
</LanguageSelectWrapper> |
||||
</Form> |
||||
</CenterBlock> |
||||
) |
||||
} |
||||
|
||||
export default Login |
||||
@ -0,0 +1,31 @@ |
||||
import { Link } from 'react-router-dom' |
||||
|
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { outlineButtonStyles } from 'features/Common/Button' |
||||
|
||||
export const RegisterButton = styled(Link)` |
||||
${outlineButtonStyles} |
||||
width: 100%; |
||||
height: 50px; |
||||
border-radius: 5px; |
||||
margin-top: 15px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-size: 20px; |
||||
${isMobileDevice |
||||
? css` |
||||
height: 44px; |
||||
margin-top: 12px; |
||||
font-weight: 500; |
||||
font-size: 17px; |
||||
border-radius: 10px; |
||||
@media screen and (orientation: landscape) { |
||||
margin: 12px auto 0; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
@ -0,0 +1,27 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { client } from '../../config/clients' |
||||
|
||||
type Props = { |
||||
height?: number, |
||||
width?: number, |
||||
} |
||||
|
||||
export const Logo = styled.div<Props>` |
||||
${client.styles.logo}; |
||||
display: block; |
||||
background-size: contain; |
||||
background-repeat: no-repeat; |
||||
${isMobileDevice |
||||
? css` |
||||
width: 207px; |
||||
height: 48px; |
||||
@media screen and (orientation: landscape){ |
||||
width: 92px; |
||||
height: 22px; |
||||
} |
||||
` |
||||
: ''} |
||||
` |
||||
@ -0,0 +1,63 @@ |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { |
||||
Modal, |
||||
Wrapper, |
||||
Header, |
||||
HeaderTitle, |
||||
Body, |
||||
Footer, |
||||
ApplyButton, |
||||
// SendConfirmationButton,
|
||||
Text, |
||||
} from './styled' |
||||
|
||||
type Props = { |
||||
email: string, |
||||
handleModalClose: () => void, |
||||
isModalOpen: boolean, |
||||
} |
||||
|
||||
export const RegisterPopup = (props: Props) => { |
||||
const { |
||||
email, |
||||
handleModalClose, |
||||
isModalOpen, |
||||
} = props |
||||
|
||||
// const handleNewConfirmation = () => {
|
||||
// // TODO дописать логику для отправки доп. письма, может понадобится, когда допишут бэк
|
||||
// // console.log('send new confirmation')
|
||||
// }
|
||||
|
||||
return ( |
||||
<Modal isOpen={isModalOpen} withCloseButton={false}> |
||||
<Wrapper> |
||||
<Header> |
||||
<HeaderTitle> |
||||
<T9n t='confirm_email' /> |
||||
</HeaderTitle> |
||||
</Header> |
||||
<Body> |
||||
<Text> |
||||
<T9n t='to_email' /> |
||||
{email} |
||||
<T9n t='send_confirm' /> |
||||
</Text> |
||||
<Text> |
||||
<T9n t='confirm_2_hours' /> |
||||
</Text> |
||||
<Text> |
||||
<T9n t='check_email' /> |
||||
</Text> |
||||
</Body> |
||||
<Footer> |
||||
<ApplyButton onClick={() => handleModalClose()}>Ok</ApplyButton> |
||||
{/* <SendConfirmationButton onClick={handleNewConfirmation}> |
||||
<T9n t='send_new_email' /> |
||||
</SendConfirmationButton> */} |
||||
</Footer> |
||||
</Wrapper> |
||||
</Modal> |
||||
) |
||||
} |
||||
@ -0,0 +1,164 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
import { devices } from 'config/devices' |
||||
|
||||
import { ModalWindow } from 'features/Modal/styled' |
||||
import { Modal as BaseModal } from 'features/Modal' |
||||
import { Header as BaseHeader } from 'features/PopupComponents' |
||||
|
||||
import { ButtonSolid, ButtonOutline } from 'features/Common' |
||||
|
||||
export const Modal = styled(BaseModal)` |
||||
background-color: rgba(0, 0, 0, 0.7); |
||||
padding: 0 60px; |
||||
|
||||
${ModalWindow} { |
||||
max-width: 757px; |
||||
min-height: 414px; |
||||
padding-top: 60px; |
||||
background-color: #333333; |
||||
border-radius: 5px; |
||||
|
||||
@media (max-width: 1370px) { |
||||
width: 70rem; |
||||
height: auto; |
||||
} |
||||
|
||||
|
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
height: auto; |
||||
top: -7vh; |
||||
} |
||||
` |
||||
: ''}; |
||||
} |
||||
` |
||||
|
||||
type WrapperProps = { |
||||
isFetching?: boolean, |
||||
} |
||||
|
||||
export const Wrapper = styled.div<WrapperProps>` |
||||
${({ isFetching }) => ( |
||||
isFetching |
||||
? css`pointer-events: none;` |
||||
: '' |
||||
)} |
||||
` |
||||
|
||||
export const Header = styled(BaseHeader)` |
||||
height: auto; |
||||
padding-top: 60; |
||||
justify-content: center; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
padding-top: 33px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const HeaderTitle = styled.span` |
||||
font-weight: 700; |
||||
font-size: 24px; |
||||
line-height: 24px; |
||||
color: #FFFFFF; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
} |
||||
|
||||
@media (orientation: landscape) { |
||||
font-size: 20px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const Body = styled.div` |
||||
padding: 25px 94px 0 78px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
font-weight: normal; |
||||
font-size: 20px; |
||||
line-height: 27px; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
padding: 13px 25px 0; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
@media (orientation: landscape){ |
||||
padding: 22px 23px 0 29px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const Footer = styled.div` |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
padding: 1.89rem; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
flex-direction: column; |
||||
padding: 20px 25px 20px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const ApplyButton = styled(ButtonSolid)` |
||||
width: 270px; |
||||
border-radius: 5px; |
||||
font-weight: 600; |
||||
margin-right: 24px; |
||||
width: 134px; |
||||
height: 50px; |
||||
font-size: 20px; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
width: 100%; |
||||
min-height: 42px; |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
@media (orientation: landscape){ |
||||
width: 290px; |
||||
min-height: 42px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const SendConfirmationButton = styled(ButtonOutline)` |
||||
width: 100%; |
||||
height: 50px; |
||||
border-radius: 5px; |
||||
font-weight: 500; |
||||
font-size: 20px; |
||||
${isMobileDevice |
||||
? css` |
||||
@media ${devices.mobile}{ |
||||
width: 100%; |
||||
} |
||||
|
||||
@media (orientation: landscape){ |
||||
width: 290px; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
export const Text = styled.span` |
||||
margin-bottom: 20px; |
||||
` |
||||
@ -0,0 +1,72 @@ |
||||
import { SyntheticEvent, useState } from 'react' |
||||
|
||||
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields' |
||||
|
||||
import { registerCheck } from '../../requests/register' |
||||
|
||||
export const useRegistrationForm = () => { |
||||
const [authError, setAuthError] = useState('') |
||||
const [termsAccepted, setTermsAccepted] = useState(false) |
||||
const [isFetching, setIsFetching] = useState(false) |
||||
const [isModalOpen, setIsModalOpen] = useState(false) |
||||
const { |
||||
email, |
||||
error: formError, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
password, |
||||
} = useAuthFields() |
||||
|
||||
const isSubmitDisabled = ( |
||||
!email |
||||
|| !password |
||||
|| !termsAccepted |
||||
|| Boolean(formError) |
||||
|| isFetching |
||||
) |
||||
|
||||
const handleSubmit = async (event: SyntheticEvent) => { |
||||
event?.preventDefault() |
||||
setIsFetching(true) |
||||
try { |
||||
await registerCheck({ |
||||
email, |
||||
password, |
||||
}) |
||||
setAuthError('') |
||||
setIsFetching(false) |
||||
setIsModalOpen(true) |
||||
} catch (err) { |
||||
setAuthError(String(err)) |
||||
setIsFetching(false) |
||||
} |
||||
} |
||||
|
||||
const handleModalClose = () => { |
||||
setIsModalOpen(false) |
||||
} |
||||
|
||||
const onTermsChange = () => { |
||||
setTermsAccepted(!termsAccepted) |
||||
} |
||||
|
||||
return { |
||||
authError, |
||||
email, |
||||
formError, |
||||
handleModalClose, |
||||
handleSubmit, |
||||
isFetching, |
||||
isModalOpen, |
||||
isSubmitDisabled, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
onTermsChange, |
||||
password, |
||||
termsAccepted, |
||||
} |
||||
} |
||||
@ -0,0 +1,125 @@ |
||||
import { useHistory } from 'react-router' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { Checkbox } from 'features/Common/Checkbox' |
||||
import { ArrowLoader } from 'features/ArrowLoader' |
||||
import { RegisterPopup } from 'features/AuthServiceApp/components/RegisterPopup' |
||||
|
||||
import { LanguageSelect } from '../LanguageSelect' |
||||
import { PasswordInput } from '../PasswordInput' |
||||
import { Input } from '../Input' |
||||
import { Logo } from '../Logo' |
||||
import { useRegistrationForm } from './hooks' |
||||
import { |
||||
BlockTitle, |
||||
CenterBlock, |
||||
InputGroup, |
||||
ButtonsBlock, |
||||
Form, |
||||
ButtonSolid, |
||||
Error, |
||||
LanguageSelectWrapper, |
||||
} from '../../styled' |
||||
import { |
||||
Label, |
||||
Link, |
||||
ButtonOutline, |
||||
CheckboxWrapper, |
||||
} from './styled' |
||||
|
||||
const Registration = () => { |
||||
const history = useHistory() |
||||
const { |
||||
authError, |
||||
email, |
||||
formError, |
||||
handleModalClose, |
||||
handleSubmit, |
||||
isFetching, |
||||
isModalOpen, |
||||
isSubmitDisabled, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
onTermsChange, |
||||
password, |
||||
termsAccepted, |
||||
} = useRegistrationForm() |
||||
|
||||
return ( |
||||
<CenterBlock> |
||||
<Logo /> |
||||
<Form onSubmit={handleSubmit}> |
||||
<BlockTitle t='step_title_registration' /> |
||||
|
||||
<InputGroup> |
||||
<Input |
||||
type='email' |
||||
name='email' |
||||
autoComplete='email' |
||||
placeholderLexic='form_email' |
||||
value={email} |
||||
onChange={onEmailChange} |
||||
onBlur={onEmailBlur} |
||||
/> |
||||
<PasswordInput |
||||
type='password' |
||||
name='password' |
||||
autoComplete='current-password' |
||||
placeholderLexic='form_password' |
||||
value={password} |
||||
onChange={onPasswordChange} |
||||
onBlur={onPasswordBlur} |
||||
/> |
||||
</InputGroup> |
||||
<Error> |
||||
{formError |
||||
? <T9n t={formError} /> |
||||
: <T9n t={authError} />} |
||||
</Error> |
||||
|
||||
<CheckboxWrapper> |
||||
<Checkbox |
||||
checked={termsAccepted} |
||||
onChange={onTermsChange} |
||||
label={( |
||||
<Label> |
||||
<T9n t='i_accept' /> |
||||
<Link href='/terms-and-conditions.html' target='_blank'> |
||||
<T9n t='terms_and_conditions' /> |
||||
</Link> |
||||
</Label> |
||||
)} |
||||
/> |
||||
</CheckboxWrapper> |
||||
|
||||
<ButtonsBlock> |
||||
<ButtonSolid disabled={isSubmitDisabled} type='submit'> |
||||
{ |
||||
isFetching |
||||
? <ArrowLoader /> |
||||
: <T9n t='sign_up' /> |
||||
} |
||||
</ButtonSolid> |
||||
<ButtonOutline |
||||
type='button' |
||||
onClick={history.goBack} |
||||
> |
||||
<T9n t='go_back' /> |
||||
</ButtonOutline> |
||||
</ButtonsBlock> |
||||
<LanguageSelectWrapper> |
||||
<LanguageSelect /> |
||||
</LanguageSelectWrapper> |
||||
<RegisterPopup |
||||
email={email} |
||||
isModalOpen={isModalOpen} |
||||
handleModalClose={handleModalClose} |
||||
/> |
||||
</Form> |
||||
</CenterBlock> |
||||
) |
||||
} |
||||
|
||||
export default Registration |
||||
@ -0,0 +1,43 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { ButtonOutline as ButtonOutlineBase } from 'features/Common/Button' |
||||
|
||||
export const ButtonOutline = styled(ButtonOutlineBase)` |
||||
width: 100%; |
||||
height: 50px; |
||||
margin-top: 15px; |
||||
border-radius: 5px; |
||||
font-weight: normal; |
||||
font-size: 20px; |
||||
${isMobileDevice |
||||
? css` |
||||
display: block; |
||||
height: 44px; |
||||
margin-top: 12px; |
||||
font-weight: 600; |
||||
font-size: 17px; |
||||
border-radius: 10px; |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const CheckboxWrapper = styled.div` |
||||
width: 100%; |
||||
display: flex; |
||||
margin-top: 4px; |
||||
margin-bottom: 24px; |
||||
` |
||||
|
||||
export const Label = styled.span` |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 21px; |
||||
` |
||||
|
||||
export const Link = styled.a` |
||||
color: ${({ theme }) => theme.colors.text100}; |
||||
text-decoration: underline; |
||||
margin-left: 6px; |
||||
` |
||||
@ -0,0 +1,40 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { facr as platformFacr } from 'config/clients/facr' |
||||
|
||||
import type { ClientConfig } from './types' |
||||
|
||||
const Background = styled.div` |
||||
position: relative; |
||||
width: 100%; |
||||
min-height: 100vh; |
||||
display: flex; |
||||
justify-content: center; |
||||
background-color: #00257A; |
||||
` |
||||
|
||||
export const facr: ClientConfig = { |
||||
...platformFacr, |
||||
background: Background, |
||||
name: 'facr', |
||||
styles: { |
||||
input: css` |
||||
background-color: transparent; |
||||
:not(:last-child) { |
||||
border-color: ${({ theme }) => theme.colors.text100}; |
||||
} |
||||
`,
|
||||
inputGroup: css` |
||||
border: 1px solid ${({ theme }) => theme.colors.text100}; |
||||
`,
|
||||
logo: css` |
||||
background-image: url(/images/facr_auth_logo.png); |
||||
height: 163px; |
||||
width: 213px; |
||||
`,
|
||||
submitButton: css` |
||||
background-color: ${({ theme }) => theme.colors.text100}; |
||||
color: #00257A; |
||||
`,
|
||||
}, |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
import { ClientIds } from 'config/clients/types' |
||||
|
||||
import { facr } from './facr' |
||||
import { instat } from './instat' |
||||
|
||||
const clients = { |
||||
[ClientIds.Facr]: facr, |
||||
[ClientIds.Instat]: instat, |
||||
} |
||||
const params = new URLSearchParams(window.location.search) |
||||
const clientId = params.get('client_id') as ClientIds |
||||
|
||||
export const client = clients[clientId] || instat |
||||
@ -0,0 +1,20 @@ |
||||
import { css } from 'styled-components' |
||||
|
||||
import { instat as platformInstat } from 'config/clients/instat' |
||||
|
||||
import { Background } from 'features/Background' |
||||
|
||||
import type { ClientConfig } from './types' |
||||
|
||||
export const instat: ClientConfig = { |
||||
...platformInstat, |
||||
background: Background, |
||||
name: 'instat', |
||||
styles: { |
||||
logo: css` |
||||
background-image: url(/images/logo.svg); |
||||
height: 54px; |
||||
width: 234px; |
||||
`,
|
||||
}, |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
import type { ReactNode, FC } from 'react' |
||||
import { css } from 'styled-components/macro' |
||||
|
||||
import type { Languages } from 'config/languages' |
||||
|
||||
type StyledCss = ReturnType<typeof css> |
||||
|
||||
export type ClientConfig = { |
||||
background: FC<{ children: ReactNode }>, |
||||
defaultLanguage?: Languages, |
||||
description: string, |
||||
name: string, |
||||
styles: { |
||||
input?: StyledCss, |
||||
inputGroup?: StyledCss, |
||||
logo: StyledCss, |
||||
submitButton?: StyledCss, |
||||
}, |
||||
title: string, |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
export const lexics = { |
||||
check_email: 15907, |
||||
check_password: 15842, |
||||
confirm_2_hours: 15906, |
||||
confirm_email: 15432, |
||||
error_email_already_in_use: 11156, |
||||
error_empty_email: 2498, |
||||
error_empty_password: 2499, |
||||
error_failed_to_send_email: 15902, |
||||
error_invalid_email_format: 12908, |
||||
error_invalid_email_or_password: 15774, |
||||
error_invalid_platform: 15925, |
||||
error_missing_required_argument: 15921, |
||||
error_simple_password: 12940, |
||||
error_unsupported_response_type: 15922, |
||||
error_user_already_created: 15926, |
||||
error_user_not_found: 1417, |
||||
form_email: 12912, |
||||
form_password: 751, |
||||
go_back: 1907, |
||||
i_accept: 15737, |
||||
login: 13404, |
||||
register: 13328, |
||||
registration_successful: 12945, |
||||
send_confirm: 15905, |
||||
send_new_email: 15830, |
||||
sign_up: 1305, |
||||
step_title_login: 13404, |
||||
step_title_registration: 1306, |
||||
terms_and_conditions: 15738, |
||||
to_email: 15904, |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
export const PAGES = { |
||||
login: '/authorize', |
||||
registration: '/registration', |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
import { ENV } from 'config/env' |
||||
|
||||
const APIS = { |
||||
preproduction: '', |
||||
production: 'https://auth.instat.tv', |
||||
staging: 'https://test-auth.instat.tv', |
||||
} |
||||
|
||||
export const API_ROOT = APIS[ENV] |
||||
|
||||
export const getApiUrl = (path: string) => ( |
||||
`${API_ROOT}${path}${window.location.search}` |
||||
) |
||||
@ -0,0 +1,22 @@ |
||||
import { isValidEmail } from '..' |
||||
|
||||
it('invalid emails', () => { |
||||
expect(isValidEmail('a')).toBeFalsy() |
||||
expect(isValidEmail('1')).toBeFalsy() |
||||
expect(isValidEmail('a@')).toBeFalsy() |
||||
expect(isValidEmail('a@m')).toBeFalsy() |
||||
expect(isValidEmail('a@m.')).toBeFalsy() |
||||
expect(isValidEmail('a@mail')).toBeFalsy() |
||||
expect(isValidEmail('a@mail.')).toBeFalsy() |
||||
expect(isValidEmail('abcd.mail.com')).toBeFalsy() |
||||
}) |
||||
|
||||
it('valid emails', () => { |
||||
expect(isValidEmail('a@mail.com')).toBeTruthy() |
||||
expect(isValidEmail('A@MAIL.COM')).toBeTruthy() |
||||
expect(isValidEmail('123@mail.com')).toBeTruthy() |
||||
expect(isValidEmail('A123@mail.com')).toBeTruthy() |
||||
expect(isValidEmail('a.b@mail.com')).toBeTruthy() |
||||
expect(isValidEmail('a-b-c@mail.com')).toBeTruthy() |
||||
expect(isValidEmail('a-b@a-b.com')).toBeTruthy() |
||||
}) |
||||
@ -0,0 +1,9 @@ |
||||
import size from 'lodash/size' |
||||
|
||||
export const emailRegex = '[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?' |
||||
|
||||
const emailRegExp = new RegExp(emailRegex) |
||||
|
||||
export const isValidEmail = (email: string) => ( |
||||
emailRegExp.test(email.toLowerCase()) && size(email) <= 100 |
||||
) |
||||
@ -0,0 +1,58 @@ |
||||
import type { |
||||
ChangeEvent, |
||||
FocusEvent, |
||||
} from 'react' |
||||
import { useState } from 'react' |
||||
|
||||
import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail' |
||||
import { isValidPassword } from 'features/AuthServiceApp/helpers/isValidPassword' |
||||
|
||||
export const useAuthFields = () => { |
||||
const [email, setEmail] = useState('') |
||||
const [password, setPassword] = useState('') |
||||
const [error, setError] = useState('') |
||||
|
||||
const checkPassword = (pass: string) => { |
||||
const regexp = /^(?=[^.,'"-])[\w\d@$!%*#?&;*]{8,}$/g |
||||
return regexp.test(pass) |
||||
} |
||||
|
||||
const onEmailChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => { |
||||
setError('') |
||||
setEmail(value) |
||||
} |
||||
|
||||
const onPasswordChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => { |
||||
setError('') |
||||
setPassword(value) |
||||
if (!checkPassword(value)) { |
||||
setError('check_password') |
||||
} |
||||
} |
||||
|
||||
const onEmailBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => { |
||||
if (!value) { |
||||
setError('error_empty_email') |
||||
} else if (!isValidEmail(value)) { |
||||
setError('error_invalid_email_format') |
||||
} |
||||
} |
||||
|
||||
const onPasswordBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => { |
||||
if (!value) { |
||||
setError('error_empty_password') |
||||
} else if (!isValidPassword(value)) { |
||||
setError('error_simple_password') |
||||
} |
||||
} |
||||
|
||||
return { |
||||
email, |
||||
error, |
||||
onEmailBlur, |
||||
onEmailChange, |
||||
onPasswordBlur, |
||||
onPasswordChange, |
||||
password, |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
import { BrowserRouter } from 'react-router-dom' |
||||
|
||||
import { getLanguageUrlParam } from 'helpers/languageUrlParam' |
||||
import { |
||||
setClientTitleAndDescription, |
||||
setClientIcons, |
||||
} from 'helpers/setClientHeads' |
||||
|
||||
import { GlobalStyles } from 'features/GlobalStyles' |
||||
import { LexicsStore } from 'features/LexicsStore' |
||||
import { Theme } from 'features/Theme' |
||||
|
||||
import { App } from './components/App' |
||||
import { client } from './config/clients' |
||||
|
||||
setClientTitleAndDescription(client.title, client.description) |
||||
setClientIcons(client.name) |
||||
|
||||
const Background = client.background |
||||
const initialLanguage = getLanguageUrlParam() || client.defaultLanguage |
||||
|
||||
const AuthServiceApp = () => ( |
||||
<BrowserRouter> |
||||
<Theme> |
||||
<GlobalStyles /> |
||||
<LexicsStore initialLanguage={initialLanguage}> |
||||
<Background> |
||||
<App /> |
||||
</Background> |
||||
</LexicsStore> |
||||
</Theme> |
||||
</BrowserRouter> |
||||
) |
||||
|
||||
export default AuthServiceApp |
||||
@ -0,0 +1,36 @@ |
||||
import { getApiUrl } from '../config/routes' |
||||
|
||||
const errorLexics = { |
||||
1: 'error_invalid_email_or_password', |
||||
4: 'error_user_not_found', |
||||
} |
||||
|
||||
type FailedResponse = { |
||||
error: { |
||||
code: keyof typeof errorLexics, |
||||
message?: string, |
||||
}, |
||||
ok: false, |
||||
} |
||||
|
||||
type SuccessResponse = { |
||||
ok: true, |
||||
} |
||||
|
||||
export const loginCheck = async (email: string, password: string) => { |
||||
const url = getApiUrl('/authorize-check') |
||||
const init: RequestInit = { |
||||
body: new URLSearchParams({ |
||||
email, |
||||
password, |
||||
}), |
||||
method: 'POST', |
||||
} |
||||
const response = await fetch(url, init) |
||||
|
||||
const body: SuccessResponse | FailedResponse = await response.json() |
||||
|
||||
if (body.ok) return Promise.resolve() |
||||
|
||||
return Promise.reject(errorLexics[body.error.code]) |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
import { getApiUrl } from 'features/AuthServiceApp/config/routes' |
||||
|
||||
const errorLexics = { |
||||
1: 'error_invalid_email_or_password', |
||||
2: 'error_missing_required_argument', |
||||
3: 'error_unsupported_response_type', |
||||
7: 'error_invalid_platform', |
||||
8: 'error_failed_to_send_email', |
||||
9: 'error_user_already_created', |
||||
} |
||||
|
||||
type FailedResponse = { |
||||
error: { |
||||
code: keyof typeof errorLexics, |
||||
message?: string, |
||||
}, |
||||
ok: false, |
||||
} |
||||
|
||||
type SuccessResponse = { |
||||
ok: true, |
||||
} |
||||
|
||||
type RegisterProps = { |
||||
email: string, |
||||
password: string, |
||||
} |
||||
|
||||
export const registerCheck = async ({ |
||||
email, |
||||
password, |
||||
} : RegisterProps) => { |
||||
const url = getApiUrl('/registration') |
||||
|
||||
const init: RequestInit = { |
||||
body: new URLSearchParams({ |
||||
email, |
||||
password, |
||||
}), |
||||
method: 'POST', |
||||
} |
||||
const response = await fetch(url, init) |
||||
|
||||
const body: SuccessResponse | FailedResponse = await response.json() |
||||
|
||||
if (body.ok) return Promise.resolve() |
||||
|
||||
return Promise.reject(errorLexics[body.error.code]) |
||||
} |
||||
@ -0,0 +1,125 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { ButtonSolid as BaseButtonSolid } from 'features/Common/Button' |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { client } from 'features/AuthServiceApp/config/clients' |
||||
|
||||
export const InputGroup = styled.div` |
||||
width: 100%; |
||||
box-shadow: ${({ theme }) => theme.colors.shadow}; |
||||
border-radius: 14px; |
||||
overflow: hidden; |
||||
|
||||
${client.styles.inputGroup} |
||||
` |
||||
|
||||
export const CenterBlock = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: flex-start; |
||||
margin-top: 9.15rem; |
||||
${isMobileDevice |
||||
? css` |
||||
margin-top: 107px; |
||||
@media screen and (orientation: landscape) { |
||||
width: 290px; |
||||
margin: auto; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const Form = styled.form` |
||||
width: 100%; |
||||
max-width: 493px; |
||||
margin-top: 1.82rem; |
||||
margin-bottom: 6.604rem; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
${isMobileDevice |
||||
? css` |
||||
width: 100%; |
||||
margin-top: 78px; |
||||
@media screen and (orientation: landscape){ |
||||
margin-bottom: 0; |
||||
margin-top: 10px; |
||||
padding: 0; |
||||
} |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
export const BlockTitle = styled(T9n)` |
||||
display: block; |
||||
font-style: normal; |
||||
font-weight: bold; |
||||
font-size: 24px; |
||||
height: 24px; |
||||
color: ${({ theme: { colors } }) => colors.text100}; |
||||
margin-bottom: 30px; |
||||
${isMobileDevice |
||||
? css` |
||||
font-size: 20px; |
||||
margin-bottom: 12px; |
||||
` |
||||
: ''}; |
||||
` |
||||
|
||||
type ButtonsBlockProps = { |
||||
marginTop?: number, |
||||
} |
||||
|
||||
export const ButtonsBlock = styled.div<ButtonsBlockProps>` |
||||
width: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: flex-start; |
||||
margin-top: ${({ marginTop = 0 }) => marginTop}px; |
||||
` |
||||
|
||||
type ButtonSolidProps = { |
||||
width?: string, |
||||
} |
||||
|
||||
export const ButtonSolid = styled(BaseButtonSolid)<ButtonSolidProps>` |
||||
width: 100%; |
||||
height: 50px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-radius: 5px; |
||||
font-weight: 600; |
||||
font-size: 20px; |
||||
${isMobileDevice |
||||
? css` |
||||
height: 44px; |
||||
font-weight: 600; |
||||
font-size: 17px; |
||||
border-radius: 10px; |
||||
` |
||||
: ''}; |
||||
|
||||
${client.styles.submitButton} |
||||
` |
||||
|
||||
export const Error = styled.span` |
||||
align-self: flex-start; |
||||
height: 20px; |
||||
margin-left: 2px; |
||||
color: ${({ theme }) => theme.colors.error}; |
||||
font-style: normal; |
||||
font-weight: 500; |
||||
font-size: 12px; |
||||
line-height: 20px; |
||||
letter-spacing: 0.1px; |
||||
` |
||||
|
||||
export const LanguageSelectWrapper = styled.div` |
||||
margin-top: 15px; |
||||
align-self: flex-end; |
||||
` |
||||
@ -1,14 +0,0 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
export const InputGroup = styled.div` |
||||
width: 100%; |
||||
box-shadow: ${({ theme }) => theme.colors.shadow}; |
||||
border-radius: 14px; |
||||
overflow: hidden; |
||||
${isMobileDevice |
||||
? css` |
||||
border-radius: 8px; |
||||
` |
||||
: ''};
|
||||
` |
||||
@ -1,16 +1,21 @@ |
||||
import { ReactNode } from 'react' |
||||
|
||||
import { client } from 'config/clients' |
||||
import { getLanguageUrlParam } from 'helpers/languageUrlParam' |
||||
|
||||
import { AuthStore } from 'features/AuthStore' |
||||
import { LexicsStore } from 'features/LexicsStore' |
||||
|
||||
const initialLanguage = getLanguageUrlParam() || client.defaultLanguage |
||||
|
||||
type Props = { |
||||
children: ReactNode, |
||||
} |
||||
|
||||
export const GlobalStores = ({ children }: Props) => ( |
||||
<AuthStore> |
||||
<LexicsStore> |
||||
<LexicsStore initialLanguage={initialLanguage}> |
||||
<AuthStore> |
||||
{children} |
||||
</LexicsStore> |
||||
</AuthStore> |
||||
</AuthStore> |
||||
</LexicsStore> |
||||
) |
||||
|
||||
@ -0,0 +1,14 @@ |
||||
import type { Languages } from 'config/languages' |
||||
import { history } from 'config/history' |
||||
|
||||
const KEY = 'lang' |
||||
|
||||
export const getLanguageUrlParam = () => ( |
||||
new URLSearchParams(history.location.search).get(KEY) as Languages |
||||
) |
||||
|
||||
export const addLanguageUrlParam = (lang: Languages, url: string) => { |
||||
const urlObject = new URL(url) |
||||
urlObject.searchParams.set(KEY, lang) |
||||
return urlObject.toString() |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
export const setClientTitleAndDescription = (title: string, desc: string) => { |
||||
document.title = title |
||||
|
||||
const description = document.querySelector('meta[name=description]') as HTMLMetaElement |
||||
description.content = desc |
||||
} |
||||
|
||||
export const setClientIcons = (clientName: string) => { |
||||
const faviconLink = document.createElement('link') |
||||
faviconLink.rel = 'icon' |
||||
faviconLink.href = `/clients/${clientName}/favicon/favicon.ico` |
||||
|
||||
const faviconAppleLink = document.createElement('link') |
||||
faviconLink.rel = 'apple-touch-icon' |
||||
faviconLink.type = 'image/png' |
||||
faviconLink.href = `/clients/${clientName}/favicon/apple-touch-icon.png` |
||||
|
||||
const favicon32Link = document.createElement('link') |
||||
favicon32Link.rel = 'icon' |
||||
favicon32Link.type = 'image/png' |
||||
favicon32Link.href = `/clients/${clientName}/favicon/favicon-32x32.png` |
||||
|
||||
const favicon16Link = document.createElement('link') |
||||
favicon16Link.rel = 'icon' |
||||
favicon16Link.type = 'image/png' |
||||
favicon16Link.href = `/clients/${clientName}/favicon/favicon-16x16.png` |
||||
|
||||
const manifest = document.createElement('link') |
||||
manifest.rel = 'icon' |
||||
manifest.href = `/clients/${clientName}/favicon/manifest.json` |
||||
|
||||
const { head } = window.document |
||||
head.appendChild(faviconLink) |
||||
head.appendChild(faviconAppleLink) |
||||
head.appendChild(favicon32Link) |
||||
head.appendChild(favicon16Link) |
||||
head.appendChild(manifest) |
||||
} |
||||
Loading…
Reference in new issue