merged 'origin/auth' into prepare-editor
This commit is contained in:
commit
335c69f64d
|
@ -97,6 +97,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a[href*='vk.cc/'],
|
||||||
a[href*='vk.com/'] {
|
a[href*='vk.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/vk-white.svg);
|
background-image: url(/icons/vk-white.svg);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.discours-banner {
|
.discoursBanner {
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
margin-bottom: 6.4rem;
|
margin-bottom: 6.4rem;
|
||||||
padding: 0.8rem 0 0;
|
padding: 0.8rem 0 0;
|
||||||
|
@ -7,10 +7,6 @@
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
margin-top: -6.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 3.2rem;
|
font-size: 3.2rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
@ -29,7 +25,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discours-banner__content {
|
.discoursBannerContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -47,7 +43,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discours-banner__image {
|
.discoursBannerImage {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
import './Banner.scss'
|
import styles from './Banner.module.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
|
import {clsx} from "clsx";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<div class="discours-banner">
|
<div class={styles.discoursBanner}>
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
<div class="discours-banner__content col-lg-5">
|
<div class={clsx(styles.discoursBannerContent, 'col-lg-5')}>
|
||||||
<h3>{t('Discours is created with our common effort')}</h3>
|
<h3>{t('Discours is created with our common effort')}</h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="/about/help">{t('Support us')}</a>
|
<a href="/about/help">{t('Support us')}</a>
|
||||||
|
@ -16,7 +17,7 @@ export default () => {
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 offset-lg-1 discours-banner__image">
|
<div class={clsx(styles.discoursBannerImage, 'col-lg-6 offset-lg-1')}>
|
||||||
<img src="/discours-banner.jpg" alt={t('Discours')} />
|
<img src="/discours-banner.jpg" alt={t('Discours')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,362 +0,0 @@
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import { Icon } from './Icon'
|
|
||||||
import { createEffect, createSignal, onMount } from 'solid-js'
|
|
||||||
import './AuthModal.scss'
|
|
||||||
import { Form } from 'solid-js-form'
|
|
||||||
import { t } from '../../utils/intl'
|
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
|
||||||
import { useAuthStore, signIn, register } from '../../stores/auth'
|
|
||||||
import { useValidator } from '../../utils/validators'
|
|
||||||
import { baseUrl } from '../../graphql/publicGraphQLClient'
|
|
||||||
import { ApiError } from '../../utils/apiClient'
|
|
||||||
import { handleClientRouteLinkClick } from '../../stores/router'
|
|
||||||
|
|
||||||
type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password'
|
|
||||||
|
|
||||||
const statuses: { [key: string]: string } = {
|
|
||||||
'email not found': 'No such account, please try to register',
|
|
||||||
'invalid password': 'Invalid password',
|
|
||||||
'invalid code': 'Invalid code',
|
|
||||||
'unknown error': 'Unknown error'
|
|
||||||
}
|
|
||||||
|
|
||||||
const titles = {
|
|
||||||
'sign-up': t('Create account'),
|
|
||||||
'sign-in': t('Enter the Discours'),
|
|
||||||
forget: t('Forgot password?'),
|
|
||||||
reset: t('Please, confirm your email to finish'),
|
|
||||||
resend: t('Resend code'),
|
|
||||||
password: t('Enter your new password')
|
|
||||||
}
|
|
||||||
|
|
||||||
// const isProperEmail = (email) => email && email.length > 5 && email.includes('@') && email.includes('.')
|
|
||||||
|
|
||||||
// 3rd party provider auth handler
|
|
||||||
const oauth = (provider: string): void => {
|
|
||||||
const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420')
|
|
||||||
popup?.focus()
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME !!!
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
export default (props: { code?: string; mode?: AuthMode }) => {
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
const [handshaking] = createSignal(false)
|
|
||||||
const { getModal } = useModalStore()
|
|
||||||
const [authError, setError] = createSignal<string>('')
|
|
||||||
const [mode, setMode] = createSignal<AuthMode>('sign-in')
|
|
||||||
const [validation, setValidation] = createSignal({})
|
|
||||||
const [initial, setInitial] = createSignal({})
|
|
||||||
let emailElement: HTMLInputElement | undefined
|
|
||||||
let pass2Element: HTMLInputElement | undefined
|
|
||||||
let passElement: HTMLInputElement | undefined
|
|
||||||
let codeElement: HTMLInputElement | undefined
|
|
||||||
|
|
||||||
// FIXME: restore logic
|
|
||||||
// const usedEmails = {}
|
|
||||||
// const checkEmailAsync = async (email: string) => {
|
|
||||||
// const handleChecked = (x: boolean) => {
|
|
||||||
// if (x && mode() === 'sign-up') setError(t('We know you, please try to sign in'))
|
|
||||||
// if (!x && mode() === 'sign-in') setError(t('No such account, please try to register'))
|
|
||||||
// usedEmails[email] = x
|
|
||||||
// }
|
|
||||||
// if (email in usedEmails) {
|
|
||||||
// handleChecked(usedEmails[email])
|
|
||||||
// } else if (isProperEmail(email)) {
|
|
||||||
// const { error, data } = await apiClient.q(authCheck, { email }, true)
|
|
||||||
// if (error) setError(error.message)
|
|
||||||
// if (data) handleChecked(data.isEmailUsed)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let checkEmailTimeout
|
|
||||||
// createEffect(() => {
|
|
||||||
// const email = emailElement?.value
|
|
||||||
// if (isProperEmail(email)) {
|
|
||||||
// if (checkEmailTimeout) clearTimeout(checkEmailTimeout)
|
|
||||||
// checkEmailTimeout = setTimeout(checkEmailAsync, 3000) // after 3 secs
|
|
||||||
// }
|
|
||||||
// }, [emailElement?.value])
|
|
||||||
|
|
||||||
// switching initial values and validatiors
|
|
||||||
const setupValidators = () => {
|
|
||||||
const [vs, ini] = useValidator(mode())
|
|
||||||
setValidation(vs)
|
|
||||||
setInitial(ini)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(setupValidators)
|
|
||||||
|
|
||||||
const resetError = () => {
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeMode = (newMode: AuthMode) => {
|
|
||||||
setMode(newMode)
|
|
||||||
resetError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// local auth handler
|
|
||||||
const localAuth = async () => {
|
|
||||||
console.log('[auth] native account processing')
|
|
||||||
switch (mode()) {
|
|
||||||
case 'sign-in': {
|
|
||||||
try {
|
|
||||||
await signIn({ email: emailElement?.value, password: passElement?.value })
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ApiError) {
|
|
||||||
if (error.code === 'email_not_confirmed') {
|
|
||||||
setError(t('Please, confirm email'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.code === 'user_not_found') {
|
|
||||||
setError(t('Something went wrong, check email and password'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'sign-up': {
|
|
||||||
if (pass2Element?.value !== passElement?.value) {
|
|
||||||
setError(t('Passwords are not equal'))
|
|
||||||
} else {
|
|
||||||
await register({
|
|
||||||
email: emailElement?.value,
|
|
||||||
password: passElement?.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'reset': {
|
|
||||||
// send reset-code to login with email
|
|
||||||
console.log('[auth] reset code: ' + codeElement?.value)
|
|
||||||
// TODO: authReset(codeElement?.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'resend': {
|
|
||||||
// TODO: authResend(emailElement?.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'forget': {
|
|
||||||
// shows forget mode of auth-modal
|
|
||||||
if (pass2Element?.value !== passElement?.value) {
|
|
||||||
setError(t('Passwords are not equal'))
|
|
||||||
} else {
|
|
||||||
// TODO: authForget(passElement?.value)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.log('[auth] unknown auth mode', mode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME move to handlers
|
|
||||||
createEffect(() => {
|
|
||||||
if (session()?.user?.slug && getModal() === 'auth') {
|
|
||||||
// hiding itself if finished
|
|
||||||
console.log('[auth] success, hiding modal')
|
|
||||||
hideModal()
|
|
||||||
} else if (session()?.error) {
|
|
||||||
console.log('[auth] failure, showing error')
|
|
||||||
setError(t(statuses[session().error || 'unknown error']))
|
|
||||||
} else {
|
|
||||||
console.log('[auth] session', session())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="row view" classList={{ 'view--sign-up': mode() === 'sign-up' }}>
|
|
||||||
<div class="col-sm-6 d-md-none auth-image">
|
|
||||||
<div class="auth-image__text" classList={{ show: mode() === 'sign-up' }}>
|
|
||||||
<h2>{t('Discours')}</h2>
|
|
||||||
<h4>{t(`Join the global community of authors!`)}</h4>
|
|
||||||
<p class="auth-benefits">
|
|
||||||
{t(
|
|
||||||
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine'
|
|
||||||
)}
|
|
||||||
.
|
|
||||||
{t('New stories every day and even more!')}
|
|
||||||
</p>
|
|
||||||
<p class="disclamer">
|
|
||||||
{t('By signing up you agree with our')}{' '}
|
|
||||||
<a
|
|
||||||
href="/about/terms-of-use"
|
|
||||||
onClick={(event) => {
|
|
||||||
hideModal()
|
|
||||||
handleClientRouteLinkClick(event)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('terms of use')}
|
|
||||||
</a>
|
|
||||||
, {t('personal data usage and email notifications')}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6 auth">
|
|
||||||
<Form
|
|
||||||
initialValues={initial()}
|
|
||||||
validation={validation()}
|
|
||||||
onSubmit={async (form) => {
|
|
||||||
console.log('[auth] form values', form.values)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="auth__inner">
|
|
||||||
<h4>{titles[mode()]}</h4>
|
|
||||||
<div class={`auth-subtitle ${mode() === 'forget' ? '' : 'hidden'}`}>
|
|
||||||
<Show
|
|
||||||
when={mode() === 'forget'}
|
|
||||||
fallback={
|
|
||||||
<Show when={mode() === 'reset'}>
|
|
||||||
{t('Enter the code or click the link from email to confirm')}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('Everything is ok, please give us your email address')}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Show when={authError()}>
|
|
||||||
<div class={`auth-info`}>
|
|
||||||
<ul>
|
|
||||||
<li class="warn">{authError()}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
{/*FIXME*/}
|
|
||||||
{/*<Show when={false && mode() === 'sign-up'}>*/}
|
|
||||||
{/* <div class='pretty-form__item'>*/}
|
|
||||||
{/* <input*/}
|
|
||||||
{/* id='username'*/}
|
|
||||||
{/* name='username'*/}
|
|
||||||
{/* autocomplete='username'*/}
|
|
||||||
{/* ref={usernameElement}*/}
|
|
||||||
{/* type='text'*/}
|
|
||||||
{/* placeholder={t('Username')}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* <label for='username'>{t('Username')}</label>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*</Show>*/}
|
|
||||||
<Show when={mode() !== 'reset' && mode() !== 'password'}>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
autocomplete="email"
|
|
||||||
ref={emailElement}
|
|
||||||
type="text"
|
|
||||||
placeholder={t('Email')}
|
|
||||||
/>
|
|
||||||
<label for="email">{t('Email')}</label>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'sign-up' || mode() === 'sign-in' || mode() === 'password'}>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
ref={passElement}
|
|
||||||
type="password"
|
|
||||||
placeholder={t('Password')}
|
|
||||||
/>
|
|
||||||
<label for="password">{t('Password')}</label>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'reset'}>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="resetcode"
|
|
||||||
name="resetcode"
|
|
||||||
ref={codeElement}
|
|
||||||
value={props.code}
|
|
||||||
type="text"
|
|
||||||
placeholder={t('Reset code')}
|
|
||||||
/>
|
|
||||||
<label for="resetcode">{t('Reset code')}</label>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'password' || mode() === 'sign-up'}>
|
|
||||||
<div class="pretty-form__item">
|
|
||||||
<input
|
|
||||||
id="password2"
|
|
||||||
name="password2"
|
|
||||||
ref={pass2Element}
|
|
||||||
type="password"
|
|
||||||
placeholder={t('Password again')}
|
|
||||||
autocomplete=""
|
|
||||||
/>
|
|
||||||
<label for="password2">{t('Password again')}</label>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div>
|
|
||||||
<button class="button submitbtn" disabled={handshaking()} onClick={localAuth}>
|
|
||||||
{handshaking() ? '...' : titles[mode()]}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Show when={mode() === 'sign-in'}>
|
|
||||||
<div class="auth-actions">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(ev) => {
|
|
||||||
ev.preventDefault()
|
|
||||||
changeMode('forget')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Forgot password?')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={mode() === 'sign-in' || mode() === 'sign-up'}>
|
|
||||||
<div class="social-provider">
|
|
||||||
<div class="providers-text">{t('Or continue with social network')}</div>
|
|
||||||
<div class="social">
|
|
||||||
<a href={''} class="facebook-auth" onClick={() => oauth('facebook')}>
|
|
||||||
<Icon name="facebook" />
|
|
||||||
</a>
|
|
||||||
<a href={''} class="google-auth" onClick={() => oauth('google')}>
|
|
||||||
<Icon name="google" />
|
|
||||||
</a>
|
|
||||||
<a href={''} class="vk-auth" onClick={() => oauth('vk')}>
|
|
||||||
<Icon name="vk" />
|
|
||||||
</a>
|
|
||||||
<a href={''} class="github-auth" onClick={() => oauth('github')}>
|
|
||||||
<Icon name="github" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="auth-control">
|
|
||||||
<div classList={{ show: mode() === 'sign-up' }}>
|
|
||||||
<span class="auth-link" onClick={() => changeMode('sign-in')}>
|
|
||||||
{t('I have an account')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div classList={{ show: mode() === 'sign-in' }}>
|
|
||||||
<span class="auth-link" onClick={() => changeMode('sign-up')}>
|
|
||||||
{t('I have no account yet')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div classList={{ show: mode() === 'forget' }}>
|
|
||||||
<span class="auth-link" onClick={() => changeMode('sign-in')}>
|
|
||||||
{t('I know the password')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div classList={{ show: mode() === 'reset' }}>
|
|
||||||
<span class="auth-link" onClick={() => changeMode('resend')}>
|
|
||||||
{t('Resend code')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -12,12 +12,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view--sign-up {
|
.signUp {
|
||||||
.auth-image {
|
.authImage {
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-image::before {
|
.authImage::before {
|
||||||
background: linear-gradient(0deg, rgb(20 20 20 / 80%), rgb(20 20 20 / 80%));
|
background: linear-gradient(0deg, rgb(20 20 20 / 80%), rgb(20 20 20 / 80%));
|
||||||
content: '';
|
content: '';
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -27,12 +27,12 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& ~ .close-control {
|
& ~ :global(.close-control) {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-image {
|
.authImage {
|
||||||
background: #141414 url('/auth-page.jpg') center no-repeat;
|
background: #141414 url('/auth-page.jpg') center no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -55,8 +55,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-image__text {
|
.authImageText {
|
||||||
display: none;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -70,22 +70,22 @@
|
||||||
color: rgb(255 255 255 / 70%);
|
color: rgb(255 255 255 / 70%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-image__text.show {
|
.authBenefits {
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-benefits {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disclamer {
|
.disclaimer {
|
||||||
color: #9fa1a7;
|
color: #9fa1a7;
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-actions {
|
.authActions {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
margin-top: 1.6rem;
|
margin-top: 1.6rem;
|
||||||
|
@ -107,84 +107,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.submitbtn {
|
.submitButton {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-provider {
|
.authControl {
|
||||||
border-bottom: 1px solid #141414;
|
|
||||||
border-top: 1px solid #141414;
|
|
||||||
margin-top: 1em;
|
|
||||||
padding: 0.8em 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social {
|
|
||||||
background-color: white !important;
|
|
||||||
display: flex;
|
|
||||||
margin: 0 -5px;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
cursor: pointer;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 5px;
|
|
||||||
padding: 0.5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 1.4em;
|
|
||||||
max-width: 1.8em;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-auth:hover {
|
|
||||||
img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-control {
|
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
div {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-link {
|
.authLink {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.providers-text {
|
.authSubtitle {
|
||||||
@include font-size(1.5rem);
|
|
||||||
|
|
||||||
margin-bottom: 1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-subtitle {
|
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-info {
|
.authInfo {
|
||||||
min-height: 5em;
|
min-height: 5em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
|
@ -192,8 +138,25 @@
|
||||||
.warn {
|
.warn {
|
||||||
color: #a00;
|
color: #a00;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info {
|
.validationError {
|
||||||
color: gray;
|
position: relative;
|
||||||
|
top: -8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
/* Red/500 */
|
||||||
|
color: #D00820;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #D00820;
|
||||||
|
border-color: #D00820;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
13
src/components/Nav/AuthModal/EmailConfirm.module.scss
Normal file
13
src/components/Nav/AuthModal/EmailConfirm.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.title {
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #141414;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-bottom: 52px;
|
||||||
|
}
|
41
src/components/Nav/AuthModal/EmailConfirm.tsx
Normal file
41
src/components/Nav/AuthModal/EmailConfirm.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import styles from './EmailConfirm.module.scss'
|
||||||
|
import authModalStyles from './AuthModal.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
import { onMount } from 'solid-js'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import { confirmEmail } from '../../../stores/auth'
|
||||||
|
|
||||||
|
type ConfirmEmailSearchParams = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailConfirm = () => {
|
||||||
|
const confirmedEmail = 'test@test.com'
|
||||||
|
|
||||||
|
const { searchParams } = useRouter<ConfirmEmailSearchParams>()
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const token = searchParams().token
|
||||||
|
try {
|
||||||
|
await confirmEmail(token)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
|
||||||
|
<div class={styles.text}>
|
||||||
|
{t("You've confirmed email")} {confirmedEmail}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', authModalStyles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
Перейти на главную
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
100
src/components/Nav/AuthModal/ForgotPasswordForm.tsx
Normal file
100
src/components/Nav/AuthModal/ForgotPasswordForm.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { Show } from 'solid-js/web'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { createSignal, JSX } from 'solid-js'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import { email, setEmail } from './sharedLogic'
|
||||||
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
import { isValidEmail } from './validators'
|
||||||
|
import { checkEmail, register } from '../../../stores/auth'
|
||||||
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
|
|
||||||
|
type FormFields = {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
|
export const ForgotPasswordForm = () => {
|
||||||
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
||||||
|
setEmail(newEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
|
const handleSubmit = async (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
setSubmitError('')
|
||||||
|
|
||||||
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
if (!email()) {
|
||||||
|
newValidationErrors.email = t('Please enter email')
|
||||||
|
} else if (!isValidEmail(email())) {
|
||||||
|
newValidationErrors.email = t('Invalid email')
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationErrors(newValidationErrors)
|
||||||
|
|
||||||
|
const isValid = Object.keys(newValidationErrors).length === 0
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: send mail with link to new password form
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError(error.message)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<h4>{t('Forgot password?')}</h4>
|
||||||
|
{t('Everything is ok, please give us your email address')}
|
||||||
|
<Show when={submitError()}>
|
||||||
|
<div class={styles.authInfo}>
|
||||||
|
<ul>
|
||||||
|
<li class={styles.warn}>{submitError()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
type="email"
|
||||||
|
value={email()}
|
||||||
|
placeholder={t('Email')}
|
||||||
|
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="email">{t('Email')}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
{isSubmitting() ? '...' : t('Restore password')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class={styles.authControl}>
|
||||||
|
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
|
||||||
|
{t('I know the password')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
150
src/components/Nav/AuthModal/LoginForm.tsx
Normal file
150
src/components/Nav/AuthModal/LoginForm.tsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import { Show } from 'solid-js/web'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { SocialProviders } from './SocialProviders'
|
||||||
|
import { signIn } from '../../../stores/auth'
|
||||||
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import { isValidEmail } from './validators'
|
||||||
|
import { email, setEmail } from './sharedLogic'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
|
type FormFields = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationErrors = Partial<Record<keyof FormFields, string>>
|
||||||
|
|
||||||
|
export const LoginForm = () => {
|
||||||
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
const [password, setPassword] = createSignal('')
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
||||||
|
setEmail(newEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordInput = (newPassword: string) => {
|
||||||
|
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
||||||
|
setPassword(newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
setSubmitError('')
|
||||||
|
|
||||||
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
if (!email()) {
|
||||||
|
newValidationErrors.email = t('Please enter email')
|
||||||
|
} else if (!isValidEmail(email())) {
|
||||||
|
newValidationErrors.email = t('Invalid email')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password()) {
|
||||||
|
newValidationErrors.password = t('Please enter password')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(newValidationErrors).length > 0) {
|
||||||
|
setValidationErrors(newValidationErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signIn({ email: email(), password: password() })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
if (error.code === 'email_not_confirmed') {
|
||||||
|
setSubmitError(t('Please, confirm email'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'user_not_found') {
|
||||||
|
setSubmitError(t('Something went wrong, check email and password'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitError(error.message)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<h4>{t('Enter the Discours')}</h4>
|
||||||
|
<Show when={submitError()}>
|
||||||
|
<div class={styles.authInfo}>
|
||||||
|
<ul>
|
||||||
|
<li class={styles.warn}>{submitError()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
type="email"
|
||||||
|
value={email()}
|
||||||
|
placeholder={t('Email')}
|
||||||
|
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="email">{t('Email')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().email}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="password"
|
||||||
|
type="password"
|
||||||
|
placeholder={t('Password')}
|
||||||
|
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="password">{t('Password')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().password}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().password}</div>
|
||||||
|
</Show>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
{isSubmitting() ? '...' : t('Enter')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class={styles.authActions}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
changeSearchParam('mode', 'forgot-password')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Forgot password?')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SocialProviders />
|
||||||
|
|
||||||
|
<div class={styles.authControl}>
|
||||||
|
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'register')}>
|
||||||
|
{t('I have no account yet')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
189
src/components/Nav/AuthModal/RegisterForm.tsx
Normal file
189
src/components/Nav/AuthModal/RegisterForm.tsx
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import { Show } from 'solid-js/web'
|
||||||
|
import type { JSX } from 'solid-js'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { SocialProviders } from './SocialProviders'
|
||||||
|
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import { isValidEmail } from './validators'
|
||||||
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
|
import { email, setEmail } from './sharedLogic'
|
||||||
|
import { useRouter } from '../../../stores/router'
|
||||||
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
|
type FormFields = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
|
export const RegisterForm = () => {
|
||||||
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
const { emailChecks } = useAuthStore()
|
||||||
|
|
||||||
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
|
const [name, setName] = createSignal('')
|
||||||
|
const [password, setPassword] = createSignal('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
||||||
|
setEmail(newEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmailBlur = () => {
|
||||||
|
if (isValidEmail(email())) {
|
||||||
|
checkEmail(email())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordInput = (newPassword: string) => {
|
||||||
|
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
||||||
|
setPassword(newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNameInput = (newPasswordCopy: string) => {
|
||||||
|
setValidationErrors(({ name: _notNeeded, ...rest }) => rest)
|
||||||
|
setName(newPasswordCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
setSubmitError('')
|
||||||
|
|
||||||
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
|
if (!name()) {
|
||||||
|
newValidationErrors.name = t('Please enter a name to sign your comments and publication')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email()) {
|
||||||
|
newValidationErrors.email = t('Please enter email')
|
||||||
|
} else if (!isValidEmail(email())) {
|
||||||
|
newValidationErrors.email = t('Invalid email')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password()) {
|
||||||
|
newValidationErrors.password = t('Please enter password')
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationErrors(newValidationErrors)
|
||||||
|
|
||||||
|
const emailCheckResult = await checkEmail(email())
|
||||||
|
|
||||||
|
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register({
|
||||||
|
name: name(),
|
||||||
|
email: email(),
|
||||||
|
password: password()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError && error.code === 'user_already_exists') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitError(error.message)
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<h4>{t('Create account')}</h4>
|
||||||
|
<Show when={submitError()}>
|
||||||
|
<div class={styles.authInfo}>
|
||||||
|
<ul>
|
||||||
|
<li class={styles.warn}>{submitError()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Full name')}
|
||||||
|
autocomplete=""
|
||||||
|
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="name">{t('Full name')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().name}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().name}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
type="text"
|
||||||
|
value={email()}
|
||||||
|
placeholder={t('Email')}
|
||||||
|
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
|
onBlur={handleEmailBlur}
|
||||||
|
/>
|
||||||
|
<label for="email">{t('Email')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().email}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={emailChecks()[email()]}>
|
||||||
|
<div class={styles.validationError}>
|
||||||
|
{t("This email is already taken. If it's you")},{' '}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
changeSearchParam('mode', 'login')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('enter')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="pretty-form__item">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
type="password"
|
||||||
|
placeholder={t('Password')}
|
||||||
|
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<label for="password">{t('Password')}</label>
|
||||||
|
</div>
|
||||||
|
<Show when={validationErrors().password}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().password}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
{isSubmitting() ? '...' : t('Join')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SocialProviders />
|
||||||
|
|
||||||
|
<div class={styles.authControl}>
|
||||||
|
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
|
||||||
|
{t('I have an account')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
45
src/components/Nav/AuthModal/SocialProviders.module.scss
Normal file
45
src/components/Nav/AuthModal/SocialProviders.module.scss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
.container {
|
||||||
|
border-bottom: 1px solid #141414;
|
||||||
|
border-top: 1px solid #141414;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 0.8em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social {
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 -5px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1em;
|
||||||
|
max-width: 1.8em;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.githubAuth:hover {
|
||||||
|
img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/components/Nav/AuthModal/SocialProviders.tsx
Normal file
42
src/components/Nav/AuthModal/SocialProviders.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
|
||||||
|
import styles from './SocialProviders.module.scss'
|
||||||
|
import { apiBaseUrl } from '../../../utils/config'
|
||||||
|
|
||||||
|
type Provider = 'facebook' | 'google' | 'vk' | 'github'
|
||||||
|
|
||||||
|
// 3rd party provider auth handler
|
||||||
|
const handleSocialAuthLinkClick = (event: MouseEvent, provider: Provider): void => {
|
||||||
|
event.preventDefault()
|
||||||
|
const popup = window.open(`${apiBaseUrl}/oauth/${provider}`, provider, 'width=740, height=420')
|
||||||
|
popup?.focus()
|
||||||
|
hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SocialProviders = () => {
|
||||||
|
return (
|
||||||
|
<div class={styles.container}>
|
||||||
|
<div class={styles.text}>{t('Or continue with social network')}</div>
|
||||||
|
<div class={styles.social}>
|
||||||
|
<a href="#" onClick={(event) => handleSocialAuthLinkClick(event, 'facebook')}>
|
||||||
|
<Icon name="facebook" />
|
||||||
|
</a>
|
||||||
|
<a href="#" onClick={(event) => handleSocialAuthLinkClick(event, 'google')}>
|
||||||
|
<Icon name="google" />
|
||||||
|
</a>
|
||||||
|
<a href="#" onClick={(event) => handleSocialAuthLinkClick(event, 'vk')}>
|
||||||
|
<Icon name="vk" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class={styles.githubAuth}
|
||||||
|
onClick={(event) => handleSocialAuthLinkClick(event, 'github')}
|
||||||
|
>
|
||||||
|
<Icon name="github" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
88
src/components/Nav/AuthModal/index.tsx
Normal file
88
src/components/Nav/AuthModal/index.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { Show } from 'solid-js/web'
|
||||||
|
import { createEffect, createMemo, onMount } from 'solid-js'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { LoginForm } from './LoginForm'
|
||||||
|
import { RegisterForm } from './RegisterForm'
|
||||||
|
import { ForgotPasswordForm } from './ForgotPasswordForm'
|
||||||
|
import { EmailConfirm } from './EmailConfirm'
|
||||||
|
import type { AuthModalMode, AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
|
const AUTH_MODAL_MODES: Record<AuthModalMode, AuthModalMode> = {
|
||||||
|
login: 'login',
|
||||||
|
register: 'register',
|
||||||
|
'forgot-password': 'forgot-password',
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
'confirm-email': 'confirm-email'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthModal = () => {
|
||||||
|
let rootRef: HTMLDivElement
|
||||||
|
|
||||||
|
const { searchParams } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
|
const mode = createMemo<AuthModalMode>(() => {
|
||||||
|
return AUTH_MODAL_MODES[searchParams().mode] || 'login'
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect((oldMode) => {
|
||||||
|
if (oldMode !== mode()) {
|
||||||
|
rootRef?.querySelector('input')?.focus()
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={rootRef}
|
||||||
|
class={clsx('row', styles.view)}
|
||||||
|
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
|
||||||
|
>
|
||||||
|
<div class={clsx('col-sm-6', 'd-md-none', styles.authImage)}>
|
||||||
|
<div
|
||||||
|
class={styles.authImageText}
|
||||||
|
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
|
||||||
|
>
|
||||||
|
<h2>{t('Discours')}</h2>
|
||||||
|
<h4>{t(`Join the global community of authors!`)}</h4>
|
||||||
|
<p class={styles.authBenefits}>
|
||||||
|
{t(
|
||||||
|
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine'
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
{t('New stories every day and even more!')}
|
||||||
|
</p>
|
||||||
|
<p class={styles.disclaimer}>
|
||||||
|
{t('By signing up you agree with our')}{' '}
|
||||||
|
<a
|
||||||
|
href="/about/terms-of-use"
|
||||||
|
onClick={(event) => {
|
||||||
|
hideModal()
|
||||||
|
handleClientRouteLinkClick(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('terms of use')}
|
||||||
|
</a>
|
||||||
|
, {t('personal data usage and email notifications')}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={clsx('col-sm-6', styles.auth)}>
|
||||||
|
<Show when={mode() === 'login'}>
|
||||||
|
<LoginForm />
|
||||||
|
</Show>
|
||||||
|
<Show when={mode() === 'register'}>
|
||||||
|
<RegisterForm />
|
||||||
|
</Show>
|
||||||
|
<Show when={mode() === 'forgot-password'}>
|
||||||
|
<ForgotPasswordForm />
|
||||||
|
</Show>
|
||||||
|
<Show when={mode() === 'confirm-email'}>
|
||||||
|
<EmailConfirm />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
5
src/components/Nav/AuthModal/sharedLogic.tsx
Normal file
5
src/components/Nav/AuthModal/sharedLogic.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
|
const [email, setEmail] = createSignal('')
|
||||||
|
|
||||||
|
export { email, setEmail }
|
5
src/components/Nav/AuthModal/types.ts
Normal file
5
src/components/Nav/AuthModal/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password'
|
||||||
|
|
||||||
|
export type AuthModalSearchParams = {
|
||||||
|
mode: AuthModalMode
|
||||||
|
}
|
7
src/components/Nav/AuthModal/validators.ts
Normal file
7
src/components/Nav/AuthModal/validators.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const isValidEmail = (email: string) => {
|
||||||
|
if (!email) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return email.includes('@') && email.includes('.') && email.length > 5
|
||||||
|
}
|
|
@ -4,9 +4,9 @@ import Notifications from './Notifications'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
import { Popup } from './Popup'
|
import { Popup } from './Popup'
|
||||||
import AuthModal from './AuthModal'
|
import { AuthModal } from './AuthModal'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import {useModalStore, showModal, useWarningsStore, toggleModal} from '../../stores/ui'
|
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
|
@ -21,10 +21,6 @@ const resources: { name: string; route: keyof Routes }[] = [
|
||||||
{ name: t('topics'), route: 'topics' }
|
{ name: t('topics'), route: 'topics' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleEnterClick = () => {
|
|
||||||
showModal('auth')
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
isHeaderFixed?: boolean
|
isHeaderFixed?: boolean
|
||||||
|
@ -37,27 +33,29 @@ export const Header = (props: Props) => {
|
||||||
const [fixed, setFixed] = createSignal(false)
|
const [fixed, setFixed] = createSignal(false)
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||||
// stores
|
// stores
|
||||||
const { getWarnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
const { getModal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
const { getPage } = useRouter()
|
const { page } = useRouter()
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||||
const toggleFixed = () => setFixed(!fixed())
|
const toggleFixed = () => setFixed(!fixed())
|
||||||
// effects
|
// effects
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const isFixed = fixed() || (getModal() && getModal() !== 'share');
|
const isFixed = fixed() || (modal() && modal() !== 'share');
|
||||||
|
|
||||||
document.body.classList.toggle('fixed', isFixed);
|
document.body.classList.toggle('fixed', isFixed);
|
||||||
document.body.classList.toggle(styles.fixed, isFixed && !getModal());
|
document.body.classList.toggle(styles.fixed, isFixed && !modal());
|
||||||
}, [fixed(), getModal()])
|
})
|
||||||
|
|
||||||
// derived
|
// derived
|
||||||
const authorized = createMemo(() => session()?.user?.slug)
|
const authorized = createMemo(() => session()?.user?.slug)
|
||||||
|
|
||||||
const handleBellIconClick = () => {
|
const handleBellIconClick = (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
if (!authorized()) {
|
if (!authorized()) {
|
||||||
showModal('auth')
|
showModal('auth')
|
||||||
return
|
return
|
||||||
|
@ -100,31 +98,31 @@ export const Header = (props: Props) => {
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Icon name="vk-white" class={stylesPopup.icon}/>
|
<Icon name="vk-white" class={stylesPopup.icon} />
|
||||||
VK
|
VK
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Icon name="facebook-white" class={stylesPopup.icon}/>
|
<Icon name="facebook-white" class={stylesPopup.icon} />
|
||||||
Facebook
|
Facebook
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Icon name="twitter-white" class={stylesPopup.icon}/>
|
<Icon name="twitter-white" class={stylesPopup.icon} />
|
||||||
Twitter
|
Twitter
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Icon name="telegram-white" class={stylesPopup.icon}/>
|
<Icon name="telegram-white" class={stylesPopup.icon} />
|
||||||
Telegram
|
Telegram
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Icon name="link-white" class={stylesPopup.icon}/>
|
<Icon name="link-white" class={stylesPopup.icon} />
|
||||||
{t('Copy link')}
|
{t('Copy link')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -148,7 +146,7 @@ export const Header = (props: Props) => {
|
||||||
>
|
>
|
||||||
<For each={resources}>
|
<For each={resources}>
|
||||||
{(r) => (
|
{(r) => (
|
||||||
<li classList={{ [styles.selected]: r.route === getPage().route }}>
|
<li classList={{ [styles.selected]: r.route === page().route }}>
|
||||||
<a href={getPagePath(router, r.route, null)} onClick={handleClientRouteLinkClick}>
|
<a href={getPagePath(router, r.route, null)} onClick={handleClientRouteLinkClick}>
|
||||||
{r.name}
|
{r.name}
|
||||||
</a>
|
</a>
|
||||||
|
@ -166,9 +164,9 @@ export const Header = (props: Props) => {
|
||||||
<div class={styles.usernav}>
|
<div class={styles.usernav}>
|
||||||
<div class={clsx(privateStyles.userControl, styles.userControl, 'col')}>
|
<div class={clsx(privateStyles.userControl, styles.userControl, 'col')}>
|
||||||
<div class={privateStyles.userControlItem}>
|
<div class={privateStyles.userControlItem}>
|
||||||
<a href="#auth" onClick={handleBellIconClick}>
|
<a href="#" onClick={handleBellIconClick}>
|
||||||
<div>
|
<div>
|
||||||
<Icon name="bell-white" counter={authorized() ? getWarnings().length : 1} />
|
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -183,7 +181,7 @@ export const Header = (props: Props) => {
|
||||||
when={authorized()}
|
when={authorized()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(privateStyles.userControlItem, 'loginbtn')}>
|
<div class={clsx(privateStyles.userControlItem, 'loginbtn')}>
|
||||||
<a href="#auth" onClick={handleEnterClick}>
|
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||||
<Icon name="user-anonymous" />
|
<Icon name="user-anonymous" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,8 +192,13 @@ export const Header = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<div class={styles.articleControls}>
|
<div class={styles.articleControls}>
|
||||||
<button onClick={() => {toggleModal('share')}}>
|
<button
|
||||||
<Icon name="share-outline" class={styles.icon}/>
|
onClick={() => {
|
||||||
|
// FIXME: Popup
|
||||||
|
showModal('share')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="share-outline" class={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
<a href="#comments">
|
<a href="#comments">
|
||||||
<Icon name="comments-outline" class={styles.icon} />
|
<Icon name="comments-outline" class={styles.icon} />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createEffect, createSignal, JSX, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
import type { JSX } from 'solid-js'
|
||||||
import './Modal.scss'
|
import './Modal.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
|
|
||||||
|
@ -7,23 +8,30 @@ interface ModalProps {
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = (props: ModalProps) => {
|
const keydownHandler = (e: KeyboardEvent) => {
|
||||||
const { getModal } = useModalStore()
|
if (e.key === 'Escape') hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
const wrapClick = (ev: Event) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
if ((ev.target as HTMLElement).classList.contains('modalwrap')) hideModal()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
|
const wrapClick = (event: { target: Element }) => {
|
||||||
|
if (event.target.classList.contains('modalwrap')) hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
window.addEventListener('keydown', keydownHandler)
|
||||||
if (e.key === 'Escape') hideModal()
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener('keydown', keydownHandler)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const [visible, setVisible] = createSignal(false)
|
const [visible, setVisible] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setVisible(getModal() === props.name)
|
setVisible(modal() === props.name)
|
||||||
console.debug(`[modal] ${props.name} is ${getModal() === props.name ? 'visible' : 'hidden'}`)
|
console.debug(`[auth.modal] ${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,15 +3,15 @@ import { useWarningsStore } from '../../stores/ui'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { getWarnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
|
|
||||||
const notSeen = createMemo(() => getWarnings().filter((warning) => !warning.seen))
|
const notSeen = createMemo(() => warnings().filter((warning) => !warning.seen))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={notSeen().length > 0}>
|
<Show when={notSeen().length > 0}>
|
||||||
<Portal>
|
<Portal>
|
||||||
<ul class="warns">
|
<ul class="warns">
|
||||||
<For each={getWarnings()}>{(warning) => <li>{warning.body}</li>}</For>
|
<For each={warnings()}>{(warning) => <li>{warning.body}</li>}</For>
|
||||||
</ul>
|
</ul>
|
||||||
</Portal>
|
</Portal>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
||||||
import style from './Popup.module.scss'
|
import style from './Popup.module.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
import {clsx} from 'clsx';
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
interface PopupProps {
|
interface PopupProps {
|
||||||
name: string
|
name: string
|
||||||
|
@ -10,7 +10,7 @@ interface PopupProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
const { getModal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
@ -20,14 +20,12 @@ export const Popup = (props: PopupProps) => {
|
||||||
|
|
||||||
const [visible, setVisible] = createSignal(false)
|
const [visible, setVisible] = createSignal(false)
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setVisible(getModal() === props.name)
|
setVisible(modal() === props.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={visible()}>
|
<Show when={visible()}>
|
||||||
<div class={clsx(style.popup, props.class)}>
|
<div class={clsx(style.popup, props.class)}>{props.children}</div>
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { clsx } from 'clsx'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
const { getPage } = useRouter()
|
const { page } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.userControl, 'col')}>
|
<div class={clsx(styles.userControl, 'col')}>
|
||||||
|
@ -21,7 +21,7 @@ export default () => {
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||||
<a href="/inbox">
|
<a href="/inbox">
|
||||||
{/*FIXME: replace with route*/}
|
{/*FIXME: replace with route*/}
|
||||||
<div classList={{ entered: getPage().path === '/inbox' }}>
|
<div classList={{ entered: page().path === '/inbox' }}>
|
||||||
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -29,7 +29,7 @@ export default () => {
|
||||||
<div class={styles.userControlItem}>
|
<div class={styles.userControlItem}>
|
||||||
<a href={`/${session().user?.slug}`}>
|
<a href={`/${session().user?.slug}`}>
|
||||||
{/*FIXME: replace with route*/}
|
{/*FIXME: replace with route*/}
|
||||||
<div classList={{ entered: getPage().path === `/${session().user?.slug}` }}>
|
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||||
<Userpic user={session().user as Author} />
|
<Userpic user={session().user as Author} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const ArticlePage = (props: PageProps) => {
|
||||||
const sortedArticles = props.article ? [props.article] : []
|
const sortedArticles = props.article ? [props.article] : []
|
||||||
|
|
||||||
const slug = createMemo(() => {
|
const slug = createMemo(() => {
|
||||||
const { getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
||||||
const page = getPage()
|
const page = getPage()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const AuthorPage = (props: PageProps) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
||||||
|
|
||||||
const slug = createMemo(() => {
|
const slug = createMemo(() => {
|
||||||
const { getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
||||||
const page = getPage()
|
const page = getPage()
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const SearchPage = (props: PageProps) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.searchResults))
|
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.searchResults))
|
||||||
|
|
||||||
const q = createMemo(() => {
|
const q = createMemo(() => {
|
||||||
const { getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
||||||
const page = getPage()
|
const page = getPage()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const TopicPage = (props: PageProps) => {
|
||||||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
||||||
|
|
||||||
const slug = createMemo(() => {
|
const slug = createMemo(() => {
|
||||||
const { getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
||||||
const page = getPage()
|
const page = getPage()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// FIXME: breaks on vercel, research
|
// FIXME: breaks on vercel, research
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { setLocale } from '../stores/ui'
|
import { hideModal, MODALS, setLocale, showModal } from '../stores/ui'
|
||||||
import { Component, createEffect, createMemo } from 'solid-js'
|
import { Component, createEffect, createMemo } from 'solid-js'
|
||||||
import { Routes, useRouter } from '../stores/router'
|
import { Routes, useRouter } from '../stores/router'
|
||||||
import { Dynamic, isServer } from 'solid-js/web'
|
import { Dynamic, isServer } from 'solid-js/web'
|
||||||
|
@ -47,6 +47,12 @@ import { CreatePage } from './Pages/CreatePage'
|
||||||
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||||
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
||||||
|
|
||||||
|
|
||||||
|
type RootSearchParams = {
|
||||||
|
modal: string
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
|
|
||||||
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||||
create: CreatePage,
|
create: CreatePage,
|
||||||
home: HomePage,
|
home: HomePage,
|
||||||
|
@ -68,16 +74,19 @@ const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Root = (props: PageProps) => {
|
export const Root = (props: PageProps) => {
|
||||||
const { getPage } = useRouter()
|
const { page, searchParams } = useRouter<RootSearchParams>()
|
||||||
|
|
||||||
// log.debug({ route: getPage().route })
|
createEffect(() => {
|
||||||
|
const modal = MODALS[searchParams().modal]
|
||||||
|
if (modal) {
|
||||||
|
showModal(modal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[getPage().route]
|
const result = pagesMap[page().route]
|
||||||
|
|
||||||
// log.debug('page', getPage())
|
if (!result || page().path === '/404') {
|
||||||
|
|
||||||
if (!result || getPage().path === '/404') {
|
|
||||||
return FourOuFourPage
|
return FourOuFourPage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,10 +95,10 @@ export const Root = (props: PageProps) => {
|
||||||
|
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const lang = new URLSearchParams(window.location.search).get('lang') || 'ru'
|
const lang = searchParams().lang || 'ru'
|
||||||
console.log('[root] client locale is', lang)
|
console.log('[root] client locale is', lang)
|
||||||
setLocale(lang)
|
setLocale(lang)
|
||||||
}, [window.location.search])
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Dynamic component={pageComponent()} {...props} />
|
return <Dynamic component={pageComponent()} {...props} />
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthorsStore, setSortAllBy as setSortAllAuthorsBy } from '../../stores/zine/authors'
|
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import '../../styles/AllTopics.scss'
|
import '../../styles/AllTopics.scss'
|
||||||
|
@ -22,12 +22,12 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setSortAllAuthorsBy(getSearchParams().by || 'shouts')
|
setAuthorsSort(searchParams().by || 'shouts')
|
||||||
})
|
})
|
||||||
|
|
||||||
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
|
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
|
||||||
|
|
||||||
const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
const { searchParams } = useRouter<AllAuthorsPageSearchParams>()
|
||||||
|
|
||||||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||||
return sortedAuthors().reduce((acc, author) => {
|
return sortedAuthors().reduce((acc, author) => {
|
||||||
|
@ -69,17 +69,17 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ selected: getSearchParams().by === 'shouts' }}>
|
<li classList={{ selected: searchParams().by === 'shouts' }}>
|
||||||
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
|
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
|
||||||
{t('By shouts')}
|
{t('By shouts')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: getSearchParams().by === 'rating' }}>
|
<li classList={{ selected: searchParams().by === 'rating' }}>
|
||||||
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
|
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
|
||||||
{t('By rating')}
|
{t('By rating')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: !getSearchParams().by || getSearchParams().by === 'name' }}>
|
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
|
||||||
<a href="/authors" onClick={handleClientRouteLinkClick}>
|
<a href="/authors" onClick={handleClientRouteLinkClick}>
|
||||||
{t('By alphabet')}
|
{t('By alphabet')}
|
||||||
</a>
|
</a>
|
||||||
|
@ -92,7 +92,7 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Show
|
<Show
|
||||||
when={!getSearchParams().by || getSearchParams().by === 'name'}
|
when={!searchParams().by || searchParams().by === 'name'}
|
||||||
fallback={() => (
|
fallback={() => (
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<For each={sortedAuthors()}>
|
<For each={sortedAuthors()}>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { createEffect, createMemo, For, Show } from 'solid-js'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { setSortAllBy as setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics'
|
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
@ -17,17 +17,17 @@ type AllTopicsViewProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||||
|
|
||||||
const { sortedTopics } = useTopicsStore({
|
const { sortedTopics } = useTopicsStore({
|
||||||
topics: props.topics,
|
topics: props.topics,
|
||||||
sortBy: getSearchParams().by || 'shouts'
|
sortBy: searchParams().by || 'shouts'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setSortAllTopicsBy(getSearchParams().by || 'shouts')
|
setTopicsSort(searchParams().by || 'shouts')
|
||||||
})
|
})
|
||||||
|
|
||||||
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
||||||
|
@ -66,17 +66,17 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ selected: getSearchParams().by === 'shouts' || !getSearchParams().by }}>
|
<li classList={{ selected: searchParams().by === 'shouts' || !searchParams().by }}>
|
||||||
<a href="/topics?by=shouts" onClick={handleClientRouteLinkClick}>
|
<a href="/topics?by=shouts" onClick={handleClientRouteLinkClick}>
|
||||||
{t('By shouts')}
|
{t('By shouts')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: getSearchParams().by === 'authors' }}>
|
<li classList={{ selected: searchParams().by === 'authors' }}>
|
||||||
<a href="/topics?by=authors" onClick={handleClientRouteLinkClick}>
|
<a href="/topics?by=authors" onClick={handleClientRouteLinkClick}>
|
||||||
{t('By authors')}
|
{t('By authors')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: getSearchParams().by === 'title' }}>
|
<li classList={{ selected: searchParams().by === 'title' }}>
|
||||||
<a
|
<a
|
||||||
href="/topics?by=title"
|
href="/topics?by=title"
|
||||||
onClick={(ev) => {
|
onClick={(ev) => {
|
||||||
|
@ -97,7 +97,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={getSearchParams().by === 'title'}
|
when={searchParams().by === 'title'}
|
||||||
fallback={() => (
|
fallback={() => (
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<For each={sortedTopics()}>
|
<For each={sortedTopics()}>
|
||||||
|
|
|
@ -34,10 +34,10 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
const { topicsByAuthor } = useTopicsStore()
|
const { topicsByAuthor } = useTopicsStore()
|
||||||
|
|
||||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = getSearchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
if (m === 'rating') return t('Top rated')
|
if (m === 'rating') return t('Top rated')
|
||||||
if (m === 'commented') return t('Top discussed')
|
if (m === 'commented') return t('Top discussed')
|
||||||
|
@ -51,7 +51,7 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
<div class="row group__controls">
|
<div class="row group__controls">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ selected: !getSearchParams().by || getSearchParams().by === 'recent' }}>
|
<li classList={{ selected: !searchParams().by || searchParams().by === 'recent' }}>
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
|
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
|
||||||
{t('Recent')}
|
{t('Recent')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const SearchView = (props: Props) => {
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
||||||
const [getQuery, setQuery] = createSignal(props.query)
|
const [getQuery, setQuery] = createSignal(props.query)
|
||||||
|
|
||||||
const { getSearchParams } = useRouter<SearchPageSearchParams>()
|
const { searchParams } = useRouter<SearchPageSearchParams>()
|
||||||
|
|
||||||
const handleQueryChange = (ev) => {
|
const handleQueryChange = (ev) => {
|
||||||
setQuery(ev.target.value)
|
setQuery(ev.target.value)
|
||||||
|
@ -48,7 +48,7 @@ export const SearchView = (props: Props) => {
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li
|
<li
|
||||||
classList={{
|
classList={{
|
||||||
selected: getSearchParams().by === 'relevance'
|
selected: searchParams().by === 'relevance'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a href="?by=relevance" onClick={handleClientRouteLinkClick}>
|
<a href="?by=relevance" onClick={handleClientRouteLinkClick}>
|
||||||
|
@ -57,7 +57,7 @@ export const SearchView = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
classList={{
|
classList={{
|
||||||
selected: getSearchParams().by === 'rating'
|
selected: searchParams().by === 'rating'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a href="?by=rating" onClick={handleClientRouteLinkClick}>
|
<a href="?by=rating" onClick={handleClientRouteLinkClick}>
|
||||||
|
|
|
@ -23,7 +23,7 @@ interface TopicProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicView = (props: TopicProps) => {
|
export const TopicView = (props: TopicProps) => {
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||||
|
|
||||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||||
|
@ -33,7 +33,7 @@ export const TopicView = (props: TopicProps) => {
|
||||||
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const m = getSearchParams().by
|
const m = searchParams().by
|
||||||
if (m === 'viewed') return t('Top viewed')
|
if (m === 'viewed') return t('Top viewed')
|
||||||
if (m === 'rating') return t('Top rated')
|
if (m === 'rating') return t('Top rated')
|
||||||
if (m === 'commented') return t('Top discussed')
|
if (m === 'commented') return t('Top discussed')
|
||||||
|
@ -47,7 +47,7 @@ export const TopicView = (props: TopicProps) => {
|
||||||
<div class="row group__controls">
|
<div class="row group__controls">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ selected: getSearchParams().by === 'recent' || !getSearchParams().by }}>
|
<li classList={{ selected: searchParams().by === 'recent' || !searchParams().by }}>
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
|
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
|
||||||
{t('Recent')}
|
{t('Recent')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,9 +1,26 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query ConfirmEmailQuery($code: String!) {
|
mutation ConfirmEmailMutation($code: String!) {
|
||||||
confirmEmail(code: $code) {
|
confirmEmail(code: $code) {
|
||||||
error
|
error
|
||||||
|
token
|
||||||
|
user {
|
||||||
|
_id: slug
|
||||||
|
email
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
bio
|
||||||
|
userpic
|
||||||
|
links
|
||||||
|
}
|
||||||
|
news {
|
||||||
|
unread
|
||||||
|
topics
|
||||||
|
authors
|
||||||
|
reactions
|
||||||
|
communities
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation RegisterMutation($email: String!, $password: String, $name: String) {
|
mutation RegisterMutation($email: String!, $password: String!, $name: String!) {
|
||||||
registerUser(email: $email, password: $password, name: $name) {
|
registerUser(email: $email, password: $password, name: $name) {
|
||||||
error
|
error
|
||||||
token
|
|
||||||
user {
|
|
||||||
_id: slug
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
userpic
|
|
||||||
bio
|
|
||||||
# links
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -15,9 +15,9 @@ export default gql`
|
||||||
}
|
}
|
||||||
news {
|
news {
|
||||||
unread
|
unread
|
||||||
inbox
|
|
||||||
topics
|
topics
|
||||||
authors
|
authors
|
||||||
|
reactions
|
||||||
communities
|
communities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { createClient, ClientOptions, dedupExchange, fetchExchange, Exchange } from '@urql/core'
|
import { createClient, ClientOptions, dedupExchange, fetchExchange, Exchange } from '@urql/core'
|
||||||
import { devtoolsExchange } from '@urql/devtools'
|
import { devtoolsExchange } from '@urql/devtools'
|
||||||
import { baseUrl } from './publicGraphQLClient'
|
import { isDev, apiBaseUrl } from '../utils/config'
|
||||||
import { isDev } from '../utils/config'
|
|
||||||
|
|
||||||
const TOKEN_LOCAL_STORAGE_KEY = 'token'
|
const TOKEN_LOCAL_STORAGE_KEY = 'token'
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ export const resetToken = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ClientOptions = {
|
const options: ClientOptions = {
|
||||||
url: baseUrl,
|
url: apiBaseUrl,
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
requestPolicy: 'cache-and-network',
|
requestPolicy: 'cache-and-network',
|
||||||
fetchOptions: () => {
|
fetchOptions: () => {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { ClientOptions, dedupExchange, fetchExchange, createClient, Exchange } from '@urql/core'
|
import { ClientOptions, dedupExchange, fetchExchange, createClient, Exchange } from '@urql/core'
|
||||||
import { devtoolsExchange } from '@urql/devtools'
|
import { devtoolsExchange } from '@urql/devtools'
|
||||||
import { isDev } from '../utils/config'
|
import { isDev, apiBaseUrl } from '../utils/config'
|
||||||
|
|
||||||
export const baseUrl = 'https://newapi.discours.io'
|
|
||||||
// export const baseUrl = 'http://localhost:8000'
|
|
||||||
|
|
||||||
const exchanges: Exchange[] = [dedupExchange, fetchExchange]
|
const exchanges: Exchange[] = [dedupExchange, fetchExchange]
|
||||||
|
|
||||||
|
@ -12,7 +9,7 @@ if (isDev) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ClientOptions = {
|
const options: ClientOptions = {
|
||||||
url: baseUrl,
|
url: apiBaseUrl,
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
requestPolicy: 'cache-and-network',
|
requestPolicy: 'cache-and-network',
|
||||||
exchanges
|
exchanges
|
||||||
|
|
|
@ -260,8 +260,8 @@ export type MutationRateUserArgs = {
|
||||||
|
|
||||||
export type MutationRegisterUserArgs = {
|
export type MutationRegisterUserArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
password?: InputMaybe<Scalars['String']>
|
|
||||||
name?: InputMaybe<Scalars['String']>
|
name?: InputMaybe<Scalars['String']>
|
||||||
|
password?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationRemoveAuthorArgs = {
|
export type MutationRemoveAuthorArgs = {
|
||||||
|
@ -271,6 +271,7 @@ export type MutationRemoveAuthorArgs = {
|
||||||
|
|
||||||
export type MutationSendLinkArgs = {
|
export type MutationSendLinkArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
|
lang?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationUnfollowArgs = {
|
export type MutationUnfollowArgs = {
|
||||||
|
@ -489,6 +490,7 @@ export type QueryShoutsForFeedArgs = {
|
||||||
|
|
||||||
export type QuerySignInArgs = {
|
export type QuerySignInArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
|
lang?: InputMaybe<Scalars['String']>
|
||||||
password?: InputMaybe<Scalars['String']>
|
password?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@
|
||||||
"collections": "коллекции",
|
"collections": "коллекции",
|
||||||
"community": "сообщество",
|
"community": "сообщество",
|
||||||
"email not confirmed": "email не подтвержден",
|
"email not confirmed": "email не подтвержден",
|
||||||
"enter": "войти",
|
"Enter": "Войти",
|
||||||
"feed": "лента",
|
"feed": "лента",
|
||||||
"follower": "подписчик",
|
"follower": "подписчик",
|
||||||
"invalid password": "некорректный пароль",
|
"invalid password": "некорректный пароль",
|
||||||
|
@ -147,22 +147,16 @@
|
||||||
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
|
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
|
||||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||||
"You was successfully authorized": "Вы были успешно авторизованы",
|
"You was successfully authorized": "Вы были успешно авторизованы",
|
||||||
"Help discours to grow": "Поддержка дискурса",
|
"Invalid email": "Проверьте правильность ввода почты",
|
||||||
"One time": "Единоразово",
|
"Please enter email": "Пожалуйста, введите почту",
|
||||||
"Every month": "Ежемесячно",
|
"Please enter password": "Пожалуйста, введите пароль",
|
||||||
"Another amount": "Другая сумма",
|
"Please enter password again": "Пожалуйста, введите пароль ещё рез",
|
||||||
"Just start typing...": "Просто начните...",
|
"Join": "Присоединиться",
|
||||||
"Tips and proposals": "Советы и предложения",
|
"Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте",
|
||||||
"Invite coauthors": "Пригласить соавторов",
|
"Full name": "Имя и фамилия",
|
||||||
"Tabula rasa": "С чистого листа",
|
"Restore password": "Восстановить пароль",
|
||||||
"Publication settings": "Настройки публикации",
|
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||||
"History of changes": "История правок",
|
"You've confirmed email": "Вы подтвердили почту",
|
||||||
"Undo": "Откат",
|
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
||||||
"Redo": "Повторить действие",
|
"enter": "войдите"
|
||||||
"Stop collab": "Индивидуальный режим",
|
|
||||||
"Restart collab": "Перезапустить коллаборацию",
|
|
||||||
"Start collab": "Коллаборативный режим",
|
|
||||||
"Clear": "Сбросить",
|
|
||||||
"Theme": "Режим",
|
|
||||||
"Editing conflict, please copy your version and refresh page": "Конфликт редактирования, пожалуйста, скопируйте вашу версию текста и обновите страницу"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import Mailgun from 'mailgun.js'
|
|
||||||
import FormDataPackage from 'form-data'
|
|
||||||
|
|
||||||
const domain: string = process.env.MAILGUN_DOMAIN
|
|
||||||
const key: string = process.env.MAILGUN_API_KEY
|
|
||||||
const mailgun = new Mailgun(FormDataPackage)
|
|
||||||
const mg = mailgun.client({
|
|
||||||
username: 'api',
|
|
||||||
key,
|
|
||||||
url: 'https://api.mailgun.net'
|
|
||||||
})
|
|
||||||
|
|
||||||
export type EmailContent = {
|
|
||||||
to: string | string[]
|
|
||||||
subject: string
|
|
||||||
text: string
|
|
||||||
html?: string
|
|
||||||
from?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const from = `discours.io <noreply@discours.io>`
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
const { to, subject } = req.query
|
|
||||||
const token = '' // FIXME
|
|
||||||
const text = 'Follow the link to confirm email: https://new.discours.io/confirm/' + token // TODO: use templates here
|
|
||||||
mg.messages
|
|
||||||
.create(domain, { to, subject, text, from } as EmailContent)
|
|
||||||
.then((_) => res.status(200))
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
3
src/pages/welcome.astro
Normal file
3
src/pages/welcome.astro
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
return Astro.redirect('/?modal=auth&mode=welcome')
|
||||||
|
---
|
|
@ -1,4 +1,3 @@
|
||||||
import { atom } from 'nanostores'
|
|
||||||
import type { AuthResult } from '../graphql/types.gen'
|
import type { AuthResult } from '../graphql/types.gen'
|
||||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
|
@ -13,37 +12,46 @@ export const signIn = async (params) => {
|
||||||
console.debug('signed in')
|
console.debug('signed in')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signUp = async (params) => {
|
|
||||||
const authResult = await apiClient.authRegister(params)
|
|
||||||
setSession(authResult)
|
|
||||||
setToken(authResult.token)
|
|
||||||
console.debug('signed up')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const signOut = () => {
|
export const signOut = () => {
|
||||||
|
// TODO: call backend to revoke token
|
||||||
setSession(null)
|
setSession(null)
|
||||||
resetToken()
|
resetToken()
|
||||||
console.debug('signed out')
|
console.debug('signed out')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emailChecks = atom<{ [email: string]: boolean }>({})
|
export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
||||||
|
|
||||||
export const signCheck = async (params) => {
|
export const checkEmail = async (email: string): Promise<boolean> => {
|
||||||
emailChecks.set(await apiClient.authCheckEmail(params))
|
if (emailChecks()[email]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkResult = await apiClient.authCheckEmail({ email })
|
||||||
|
|
||||||
|
if (checkResult) {
|
||||||
|
setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true }))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetCode = atom<string>()
|
export const [resetCode, setResetCode] = createSignal('')
|
||||||
|
|
||||||
export const register = async ({ email, password }: { email: string; password: string }) => {
|
export const register = async ({
|
||||||
const authResult = await apiClient.authRegister({
|
name,
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}) => {
|
||||||
|
await apiClient.authRegister({
|
||||||
|
name,
|
||||||
email,
|
email,
|
||||||
password
|
password
|
||||||
})
|
})
|
||||||
|
|
||||||
if (authResult && !authResult.error) {
|
|
||||||
console.debug('register session update', authResult)
|
|
||||||
setSession(authResult)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signSendLink = async (params) => {
|
export const signSendLink = async (params) => {
|
||||||
|
@ -51,18 +59,24 @@ export const signSendLink = async (params) => {
|
||||||
resetToken()
|
resetToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signConfirm = async (params) => {
|
|
||||||
const auth = await apiClient.authConfirmCode(params) // { code }
|
|
||||||
setToken(auth.token)
|
|
||||||
setSession(auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renewSession = async () => {
|
export const renewSession = async () => {
|
||||||
const authResult = await apiClient.getSession() // token in header
|
const authResult = await apiClient.getSession() // token in header
|
||||||
setToken(authResult.token)
|
setToken(authResult.token)
|
||||||
setSession(authResult)
|
setSession(authResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = () => {
|
export const confirmEmail = async (token: string) => {
|
||||||
return { session }
|
const authResult = await apiClient.confirmEmail({ token })
|
||||||
|
setToken(authResult.token)
|
||||||
|
setSession(authResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmEmail = async (token: string) => {
|
||||||
|
const authResult = await apiClient.confirmEmail({ token })
|
||||||
|
setToken(authResult.token)
|
||||||
|
setSession(authResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = () => {
|
||||||
|
return { session, emailChecks }
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,8 +69,11 @@ export const handleClientRouteLinkClick = (event) => {
|
||||||
const url = new URL(link.href)
|
const url = new URL(link.href)
|
||||||
if (url.origin === location.origin) {
|
if (url.origin === location.origin) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// TODO: search params
|
|
||||||
routerStore.open(url.pathname)
|
routerStore.open(url.pathname)
|
||||||
|
const params = Object.fromEntries(new URLSearchParams(url.search))
|
||||||
|
searchParamsStore.open(params)
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0
|
left: 0
|
||||||
|
@ -91,16 +94,27 @@ if (!isServer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRouter = <TSearchParams extends Record<string, string> = Record<string, string>>() => {
|
export const useRouter = <TSearchParams extends Record<string, string> = Record<string, string>>() => {
|
||||||
const getPage = useStore(routerStore)
|
const page = useStore(routerStore)
|
||||||
const getSearchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
|
const searchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
|
||||||
|
|
||||||
const changeSearchParam = <TKey extends keyof TSearchParams>(key: TKey, value: TSearchParams[TKey]) => {
|
const changeSearchParam = <TKey extends keyof TSearchParams>(
|
||||||
searchParamsStore.open({ ...searchParamsStore.get(), [key]: value })
|
key: TKey,
|
||||||
|
value: TSearchParams[TKey],
|
||||||
|
replace = false
|
||||||
|
) => {
|
||||||
|
const newSearchParams = { ...searchParamsStore.get() }
|
||||||
|
if (value === null) {
|
||||||
|
delete newSearchParams[key.toString()]
|
||||||
|
} else {
|
||||||
|
newSearchParams[key.toString()] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
searchParamsStore.open(newSearchParams, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPage,
|
page,
|
||||||
getSearchParams,
|
searchParams,
|
||||||
changeSearchParam
|
changeSearchParam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
//import { persistentAtom } from '@nanostores/persistent'
|
//import { persistentAtom } from '@nanostores/persistent'
|
||||||
import { atom } from 'nanostores'
|
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
|
import { useRouter } from './router'
|
||||||
|
|
||||||
//export const locale = persistentAtom<string>('locale', 'ru')
|
//export const locale = persistentAtom<string>('locale', 'ru')
|
||||||
export const [locale, setLocale] = createSignal('ru')
|
export const [locale, setLocale] = createSignal('ru')
|
||||||
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'share' | 'thank' | 'donate' | null
|
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'share' | 'thank' | 'donate'
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
type WarnKind = 'error' | 'warn' | 'info'
|
||||||
|
|
||||||
export interface Warning {
|
export interface Warning {
|
||||||
|
@ -14,28 +13,38 @@ export interface Warning {
|
||||||
seen?: boolean
|
seen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = atom<ModalType>(null)
|
export const MODALS: Record<ModalType, ModalType> = {
|
||||||
|
auth: 'auth',
|
||||||
|
subscribe: 'subscribe',
|
||||||
|
feedback: 'feedback',
|
||||||
|
share: 'share',
|
||||||
|
thank: 'thank',
|
||||||
|
donate: 'donate'
|
||||||
|
}
|
||||||
|
|
||||||
const warnings = atom<Warning[]>([])
|
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||||
|
|
||||||
export const showModal = (modalType: ModalType) => modal.set(modalType)
|
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
||||||
export const hideModal = () => modal.set(null)
|
|
||||||
export const toggleModal = (modalType) => modal.get() ? hideModal() : showModal(modalType)
|
|
||||||
|
|
||||||
export const clearWarns = () => warnings.set([])
|
export const showModal = (modalType: ModalType) => setModal(modalType)
|
||||||
export const warn = (warning: Warning) => warnings.set([...warnings.get(), warning])
|
export const hideModal = () => {
|
||||||
|
const { changeSearchParam } = useRouter()
|
||||||
|
changeSearchParam('modal', null, true)
|
||||||
|
changeSearchParam('mode', null, true)
|
||||||
|
setModal(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearWarns = () => setWarnings([])
|
||||||
|
export const warn = (warning: Warning) => setWarnings([...warnings(), warning])
|
||||||
|
|
||||||
export const useWarningsStore = () => {
|
export const useWarningsStore = () => {
|
||||||
const getWarnings = useStore(warnings)
|
|
||||||
return {
|
return {
|
||||||
getWarnings
|
warnings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useModalStore = () => {
|
export const useModalStore = () => {
|
||||||
const getModal = useStore(modal)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getModal
|
modal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ export type AuthorsSortBy = 'shouts' | 'name' | 'rating'
|
||||||
|
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
|
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
|
||||||
|
|
||||||
export { setSortAllBy }
|
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
||||||
const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({})
|
const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({})
|
||||||
const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({})
|
const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({})
|
||||||
|
|
|
@ -8,7 +8,7 @@ export type TopicsSortBy = 'followers' | 'title' | 'authors' | 'shouts'
|
||||||
|
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<TopicsSortBy>('shouts')
|
const [sortAllBy, setSortAllBy] = createSignal<TopicsSortBy>('shouts')
|
||||||
|
|
||||||
export { setSortAllBy }
|
export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
||||||
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
--danger-color: #fc6847;
|
--danger-color: #fc6847;
|
||||||
--lightgray-color: rgb(84 16 17 / 6%);
|
--lightgray-color: rgb(84 16 17 / 6%);
|
||||||
--font: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Open Sans',
|
--font: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Open Sans',
|
||||||
'Helvetica Neue', sans-serif;
|
'Helvetica Neue', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -277,6 +277,7 @@ form {
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
&:focus,
|
&:focus,
|
||||||
|
&:-webkit-autofill,
|
||||||
&:not(:placeholder-shown) {
|
&:not(:placeholder-shown) {
|
||||||
& ~ label {
|
& ~ label {
|
||||||
font-size: 60%;
|
font-size: 60%;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import authLogoutQuery from '../graphql/mutation/auth-logout'
|
||||||
import authLoginQuery from '../graphql/query/auth-login'
|
import authLoginQuery from '../graphql/query/auth-login'
|
||||||
import authRegisterMutation from '../graphql/mutation/auth-register'
|
import authRegisterMutation from '../graphql/mutation/auth-register'
|
||||||
import authCheckEmailQuery from '../graphql/query/auth-check-email'
|
import authCheckEmailQuery from '../graphql/query/auth-check-email'
|
||||||
import authConfirmCodeMutation from '../graphql/mutation/auth-confirm-email'
|
import authConfirmEmailMutation from '../graphql/mutation/auth-confirm-email'
|
||||||
import authSendLinkMutation from '../graphql/mutation/auth-send-link'
|
import authSendLinkMutation from '../graphql/mutation/auth-send-link'
|
||||||
import followMutation from '../graphql/mutation/follow'
|
import followMutation from '../graphql/mutation/follow'
|
||||||
import unfollowMutation from '../graphql/mutation/unfollow'
|
import unfollowMutation from '../graphql/mutation/unfollow'
|
||||||
|
@ -32,7 +32,7 @@ import myChats from '../graphql/query/my-chats'
|
||||||
const FEED_SIZE = 50
|
const FEED_SIZE = 50
|
||||||
const REACTIONS_PAGE_SIZE = 100
|
const REACTIONS_PAGE_SIZE = 100
|
||||||
|
|
||||||
type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found'
|
type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' | 'user_already_exists'
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
code: ApiErrorCode
|
code: ApiErrorCode
|
||||||
|
@ -65,12 +65,26 @@ export const apiClient = {
|
||||||
|
|
||||||
return response.data.signIn
|
return response.data.signIn
|
||||||
},
|
},
|
||||||
authRegister: async ({ email, password = '', name = '' }): Promise<AuthResult> => {
|
authRegister: async ({
|
||||||
// NOTE: name is to display
|
email,
|
||||||
|
password,
|
||||||
|
name
|
||||||
|
}: {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
name: string
|
||||||
|
}): Promise<void> => {
|
||||||
const response = await publicGraphQLClient
|
const response = await publicGraphQLClient
|
||||||
.mutation(authRegisterMutation, { email, password, name })
|
.mutation(authRegisterMutation, { email, password, name })
|
||||||
.toPromise()
|
.toPromise()
|
||||||
return response.data.registerUser
|
|
||||||
|
if (response.error) {
|
||||||
|
if (response.error.message === '[GraphQL] User already exist') {
|
||||||
|
throw new ApiError('user_already_exists', response.error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
authSignOut: async () => {
|
authSignOut: async () => {
|
||||||
const response = await publicGraphQLClient.query(authLogoutQuery, {}).toPromise()
|
const response = await publicGraphQLClient.query(authLogoutQuery, {}).toPromise()
|
||||||
|
@ -79,6 +93,7 @@ export const apiClient = {
|
||||||
authCheckEmail: async ({ email }) => {
|
authCheckEmail: async ({ email }) => {
|
||||||
// check if email is used
|
// check if email is used
|
||||||
const response = await publicGraphQLClient.query(authCheckEmailQuery, { email }).toPromise()
|
const response = await publicGraphQLClient.query(authCheckEmailQuery, { email }).toPromise()
|
||||||
|
log.debug('authCheckEmail', response)
|
||||||
return response.data.isEmailUsed
|
return response.data.isEmailUsed
|
||||||
},
|
},
|
||||||
authSendLink: async ({ email }) => {
|
authSendLink: async ({ email }) => {
|
||||||
|
@ -86,10 +101,21 @@ export const apiClient = {
|
||||||
const response = await publicGraphQLClient.query(authSendLinkMutation, { email }).toPromise()
|
const response = await publicGraphQLClient.query(authSendLinkMutation, { email }).toPromise()
|
||||||
return response.data.reset
|
return response.data.reset
|
||||||
},
|
},
|
||||||
authConfirmCode: async ({ code }) => {
|
confirmEmail: async ({ token }: { token: string }) => {
|
||||||
// confirm email with code from link
|
// confirm email with code from link
|
||||||
const response = await publicGraphQLClient.query(authConfirmCodeMutation, { code }).toPromise()
|
const response = await publicGraphQLClient
|
||||||
return response.data.reset
|
.mutation(authConfirmEmailMutation, { code: token })
|
||||||
|
.toPromise()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.confirmEmail?.error) {
|
||||||
|
throw new ApiError('unknown', response.data?.confirmEmail?.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.confirmEmail
|
||||||
},
|
},
|
||||||
|
|
||||||
getTopArticles: async () => {
|
getTopArticles: async () => {
|
||||||
|
@ -216,6 +242,16 @@ export const apiClient = {
|
||||||
getSession: async (): Promise<AuthResult> => {
|
getSession: async (): Promise<AuthResult> => {
|
||||||
// renew session with auth token in header (!)
|
// renew session with auth token in header (!)
|
||||||
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
// TODO
|
||||||
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.refreshSession?.error) {
|
||||||
|
throw new ApiError('unknown', response.data.refreshSession.error)
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.refreshSession
|
return response.data.refreshSession
|
||||||
},
|
},
|
||||||
getPublishedArticles: async ({ limit = FEED_SIZE, offset }: { limit?: number; offset?: number }) => {
|
getPublishedArticles: async ({ limit = FEED_SIZE, offset }: { limit?: number; offset?: number }) => {
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
export const isDev = import.meta.env.MODE === 'development'
|
export const isDev = import.meta.env.MODE === 'development'
|
||||||
|
|
||||||
|
// export const apiBaseUrl = 'https://newapi.discours.io'
|
||||||
|
export const apiBaseUrl = 'http://localhost:8080'
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import * as Yup from 'yup'
|
|
||||||
|
|
||||||
const validators = {
|
|
||||||
'sign-in': [
|
|
||||||
{ username: '', password: '' },
|
|
||||||
{
|
|
||||||
username: Yup.string().required(),
|
|
||||||
password: Yup.string().required()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'sign-up': [
|
|
||||||
{ username: '', password: '', email: '' },
|
|
||||||
{
|
|
||||||
username: Yup.string(),
|
|
||||||
email: Yup.string().email().required(),
|
|
||||||
password: Yup.string().required()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
forget: [{ email: '' }, { email: Yup.string().email().required() }],
|
|
||||||
reset: [{ code: '' }, { code: Yup.string().required() }],
|
|
||||||
resend: [{ email: '' }, { email: Yup.string().email().email().required() }],
|
|
||||||
password: [
|
|
||||||
{ password: '', password2: '' },
|
|
||||||
{ password: Yup.string().required(), password2: Yup.string().required() }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
export const useValidator = (name: string) => {
|
|
||||||
return validators[name] || []
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user