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/'] {
|
||||
&::before {
|
||||
background-image: url(/icons/vk-white.svg);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.discours-banner {
|
||||
.discoursBanner {
|
||||
background: #f8f8f8;
|
||||
margin-bottom: 6.4rem;
|
||||
padding: 0.8rem 0 0;
|
||||
|
@ -7,10 +7,6 @@
|
|||
font-size: 80%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: -6.4rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 3.2rem;
|
||||
font-weight: 800;
|
||||
|
@ -29,7 +25,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.discours-banner__content {
|
||||
.discoursBannerContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
@ -47,7 +43,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.discours-banner__image {
|
||||
.discoursBannerImage {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import './Banner.scss'
|
||||
import styles from './Banner.module.scss'
|
||||
import { t } from '../../utils/intl'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import {clsx} from "clsx";
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div class="discours-banner">
|
||||
<div class={styles.discoursBanner}>
|
||||
<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>
|
||||
<p>
|
||||
<a href="/about/help">{t('Support us')}</a>
|
||||
|
@ -16,7 +17,7 @@ export default () => {
|
|||
</a>
|
||||
</p>
|
||||
</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')} />
|
||||
</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 {
|
||||
.auth-image {
|
||||
.signUp {
|
||||
.authImage {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.auth-image::before {
|
||||
.authImage::before {
|
||||
background: linear-gradient(0deg, rgb(20 20 20 / 80%), rgb(20 20 20 / 80%));
|
||||
content: '';
|
||||
height: 100%;
|
||||
|
@ -27,12 +27,12 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
& ~ .close-control {
|
||||
& ~ :global(.close-control) {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-image {
|
||||
.authImage {
|
||||
background: #141414 url('/auth-page.jpg') center no-repeat;
|
||||
background-size: cover;
|
||||
color: #fff;
|
||||
|
@ -55,8 +55,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.auth-image__text {
|
||||
display: none;
|
||||
.authImageText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
@ -70,22 +70,22 @@
|
|||
color: rgb(255 255 255 / 70%);
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-image__text.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auth-benefits {
|
||||
.authBenefits {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.disclamer {
|
||||
.disclaimer {
|
||||
color: #9fa1a7;
|
||||
@include font-size(1.2rem);
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
.authActions {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
margin-top: 1.6rem;
|
||||
|
@ -107,84 +107,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.submitbtn {
|
||||
.submitButton {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.social-provider {
|
||||
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 {
|
||||
.authControl {
|
||||
color: $link-color;
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
|
||||
div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
.authLink {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.providers-text {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
margin-bottom: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
.authSubtitle {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.auth-info {
|
||||
.authInfo {
|
||||
min-height: 5em;
|
||||
font-weight: 400;
|
||||
font-size: smaller;
|
||||
|
@ -192,8 +138,25 @@
|
|||
.warn {
|
||||
color: #a00;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
color: gray;
|
||||
.validationError {
|
||||
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 { Modal } from './Modal'
|
||||
import { Popup } from './Popup'
|
||||
import AuthModal from './AuthModal'
|
||||
import { AuthModal } from './AuthModal'
|
||||
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 { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||
import styles from './Header.module.scss'
|
||||
|
@ -21,10 +21,6 @@ const resources: { name: string; route: keyof Routes }[] = [
|
|||
{ name: t('topics'), route: 'topics' }
|
||||
]
|
||||
|
||||
const handleEnterClick = () => {
|
||||
showModal('auth')
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
isHeaderFixed?: boolean
|
||||
|
@ -37,27 +33,29 @@ export const Header = (props: Props) => {
|
|||
const [fixed, setFixed] = createSignal(false)
|
||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||
// stores
|
||||
const { getWarnings } = useWarningsStore()
|
||||
const { warnings } = useWarningsStore()
|
||||
const { session } = useAuthStore()
|
||||
const { getModal } = useModalStore()
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const { getPage } = useRouter()
|
||||
const { page } = useRouter()
|
||||
|
||||
// methods
|
||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
||||
const toggleFixed = () => setFixed(!fixed())
|
||||
// effects
|
||||
createEffect(() => {
|
||||
const isFixed = fixed() || (getModal() && getModal() !== 'share');
|
||||
const isFixed = fixed() || (modal() && modal() !== 'share');
|
||||
|
||||
document.body.classList.toggle('fixed', isFixed);
|
||||
document.body.classList.toggle(styles.fixed, isFixed && !getModal());
|
||||
}, [fixed(), getModal()])
|
||||
document.body.classList.toggle(styles.fixed, isFixed && !modal());
|
||||
})
|
||||
|
||||
// derived
|
||||
const authorized = createMemo(() => session()?.user?.slug)
|
||||
|
||||
const handleBellIconClick = () => {
|
||||
const handleBellIconClick = (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!authorized()) {
|
||||
showModal('auth')
|
||||
return
|
||||
|
@ -100,31 +98,31 @@ export const Header = (props: Props) => {
|
|||
<ul class="nodash">
|
||||
<li>
|
||||
<a href="#">
|
||||
<Icon name="vk-white" class={stylesPopup.icon}/>
|
||||
<Icon name="vk-white" class={stylesPopup.icon} />
|
||||
VK
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<Icon name="facebook-white" class={stylesPopup.icon}/>
|
||||
<Icon name="facebook-white" class={stylesPopup.icon} />
|
||||
Facebook
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<Icon name="twitter-white" class={stylesPopup.icon}/>
|
||||
<Icon name="twitter-white" class={stylesPopup.icon} />
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<Icon name="telegram-white" class={stylesPopup.icon}/>
|
||||
<Icon name="telegram-white" class={stylesPopup.icon} />
|
||||
Telegram
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<Icon name="link-white" class={stylesPopup.icon}/>
|
||||
<Icon name="link-white" class={stylesPopup.icon} />
|
||||
{t('Copy link')}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -148,7 +146,7 @@ export const Header = (props: Props) => {
|
|||
>
|
||||
<For each={resources}>
|
||||
{(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}>
|
||||
{r.name}
|
||||
</a>
|
||||
|
@ -166,9 +164,9 @@ export const Header = (props: Props) => {
|
|||
<div class={styles.usernav}>
|
||||
<div class={clsx(privateStyles.userControl, styles.userControl, 'col')}>
|
||||
<div class={privateStyles.userControlItem}>
|
||||
<a href="#auth" onClick={handleBellIconClick}>
|
||||
<a href="#" onClick={handleBellIconClick}>
|
||||
<div>
|
||||
<Icon name="bell-white" counter={authorized() ? getWarnings().length : 1} />
|
||||
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -183,7 +181,7 @@ export const Header = (props: Props) => {
|
|||
when={authorized()}
|
||||
fallback={
|
||||
<div class={clsx(privateStyles.userControlItem, 'loginbtn')}>
|
||||
<a href="#auth" onClick={handleEnterClick}>
|
||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||
<Icon name="user-anonymous" />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -194,8 +192,13 @@ export const Header = (props: Props) => {
|
|||
</div>
|
||||
<Show when={props.title}>
|
||||
<div class={styles.articleControls}>
|
||||
<button onClick={() => {toggleModal('share')}}>
|
||||
<Icon name="share-outline" class={styles.icon}/>
|
||||
<button
|
||||
onClick={() => {
|
||||
// FIXME: Popup
|
||||
showModal('share')
|
||||
}}
|
||||
>
|
||||
<Icon name="share-outline" class={styles.icon} />
|
||||
</button>
|
||||
<a href="#comments">
|
||||
<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 { hideModal, useModalStore } from '../../stores/ui'
|
||||
|
||||
|
@ -7,23 +8,30 @@ interface ModalProps {
|
|||
children: JSX.Element
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const { getModal } = useModalStore()
|
||||
const keydownHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') hideModal()
|
||||
}
|
||||
|
||||
const wrapClick = (ev: Event) => {
|
||||
if ((ev.target as HTMLElement).classList.contains('modalwrap')) hideModal()
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const wrapClick = (event: { target: Element }) => {
|
||||
if (event.target.classList.contains('modalwrap')) hideModal()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') hideModal()
|
||||
window.addEventListener('keydown', keydownHandler)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
})
|
||||
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
setVisible(getModal() === props.name)
|
||||
console.debug(`[modal] ${props.name} is ${getModal() === props.name ? 'visible' : 'hidden'}`)
|
||||
setVisible(modal() === props.name)
|
||||
console.debug(`[auth.modal] ${props.name} is ${modal() === props.name ? 'visible' : 'hidden'}`)
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,15 +3,15 @@ import { useWarningsStore } from '../../stores/ui'
|
|||
import { createMemo } from 'solid-js'
|
||||
|
||||
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 (
|
||||
<Show when={notSeen().length > 0}>
|
||||
<Portal>
|
||||
<ul class="warns">
|
||||
<For each={getWarnings()}>{(warning) => <li>{warning.body}</li>}</For>
|
||||
<For each={warnings()}>{(warning) => <li>{warning.body}</li>}</For>
|
||||
</ul>
|
||||
</Portal>
|
||||
</Show>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createEffect, createSignal, onMount, Show } from 'solid-js'
|
||||
import style from './Popup.module.scss'
|
||||
import { hideModal, useModalStore } from '../../stores/ui'
|
||||
import {clsx} from 'clsx';
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface PopupProps {
|
||||
name: string
|
||||
|
@ -10,7 +10,7 @@ interface PopupProps {
|
|||
}
|
||||
|
||||
export const Popup = (props: PopupProps) => {
|
||||
const { getModal } = useModalStore()
|
||||
const { modal } = useModalStore()
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
|
@ -20,14 +20,12 @@ export const Popup = (props: PopupProps) => {
|
|||
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
createEffect(() => {
|
||||
setVisible(getModal() === props.name)
|
||||
setVisible(modal() === props.name)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={visible()}>
|
||||
<div class={clsx(style.popup, props.class)}>
|
||||
{props.children}
|
||||
</div>
|
||||
<div class={clsx(style.popup, props.class)}>{props.children}</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { clsx } from 'clsx'
|
|||
|
||||
export default () => {
|
||||
const { session } = useAuthStore()
|
||||
const { getPage } = useRouter()
|
||||
const { page } = useRouter()
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.userControl, 'col')}>
|
||||
|
@ -21,7 +21,7 @@ export default () => {
|
|||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||
<a href="/inbox">
|
||||
{/*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} />
|
||||
</div>
|
||||
</a>
|
||||
|
@ -29,7 +29,7 @@ export default () => {
|
|||
<div class={styles.userControlItem}>
|
||||
<a href={`/${session().user?.slug}`}>
|
||||
{/*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} />
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
@ -11,7 +11,7 @@ export const ArticlePage = (props: PageProps) => {
|
|||
const sortedArticles = props.article ? [props.article] : []
|
||||
|
||||
const slug = createMemo(() => {
|
||||
const { getPage } = useRouter()
|
||||
const { page: getPage } = useRouter()
|
||||
|
||||
const page = getPage()
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const AuthorPage = (props: PageProps) => {
|
|||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
||||
|
||||
const slug = createMemo(() => {
|
||||
const { getPage } = useRouter()
|
||||
const { page: getPage } = useRouter()
|
||||
|
||||
const page = getPage()
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ export const SearchPage = (props: PageProps) => {
|
|||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.searchResults))
|
||||
|
||||
const q = createMemo(() => {
|
||||
const { getPage } = useRouter()
|
||||
const { page: getPage } = useRouter()
|
||||
|
||||
const page = getPage()
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const TopicPage = (props: PageProps) => {
|
|||
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
|
||||
|
||||
const slug = createMemo(() => {
|
||||
const { getPage } = useRouter()
|
||||
const { page: getPage } = useRouter()
|
||||
|
||||
const page = getPage()
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// FIXME: breaks on vercel, research
|
||||
// import 'solid-devtools'
|
||||
|
||||
import { setLocale } from '../stores/ui'
|
||||
import { hideModal, MODALS, setLocale, showModal } from '../stores/ui'
|
||||
import { Component, createEffect, createMemo } from 'solid-js'
|
||||
import { Routes, useRouter } from '../stores/router'
|
||||
import { Dynamic, isServer } from 'solid-js/web'
|
||||
|
@ -47,6 +47,12 @@ import { CreatePage } from './Pages/CreatePage'
|
|||
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
||||
|
||||
|
||||
type RootSearchParams = {
|
||||
modal: string
|
||||
lang: string
|
||||
}
|
||||
|
||||
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||
create: CreatePage,
|
||||
home: HomePage,
|
||||
|
@ -68,16 +74,19 @@ const pagesMap: Record<keyof Routes, Component<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 result = pagesMap[getPage().route]
|
||||
const result = pagesMap[page().route]
|
||||
|
||||
// log.debug('page', getPage())
|
||||
|
||||
if (!result || getPage().path === '/404') {
|
||||
if (!result || page().path === '/404') {
|
||||
return FourOuFourPage
|
||||
}
|
||||
|
||||
|
@ -86,10 +95,10 @@ export const Root = (props: PageProps) => {
|
|||
|
||||
if (!isServer) {
|
||||
createEffect(() => {
|
||||
const lang = new URLSearchParams(window.location.search).get('lang') || 'ru'
|
||||
const lang = searchParams().lang || 'ru'
|
||||
console.log('[root] client locale is', lang)
|
||||
setLocale(lang)
|
||||
}, [window.location.search])
|
||||
})
|
||||
}
|
||||
|
||||
return <Dynamic component={pageComponent()} {...props} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Author } from '../../graphql/types.gen'
|
|||
import { AuthorCard } from '../Author/Card'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
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 { useAuthStore } from '../../stores/auth'
|
||||
import '../../styles/AllTopics.scss'
|
||||
|
@ -22,12 +22,12 @@ export const AllAuthorsView = (props: Props) => {
|
|||
const { session } = useAuthStore()
|
||||
|
||||
createEffect(() => {
|
||||
setSortAllAuthorsBy(getSearchParams().by || 'shouts')
|
||||
setAuthorsSort(searchParams().by || 'shouts')
|
||||
})
|
||||
|
||||
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[] }>(() => {
|
||||
return sortedAuthors().reduce((acc, author) => {
|
||||
|
@ -69,17 +69,17 @@ export const AllAuthorsView = (props: Props) => {
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="view-switcher">
|
||||
<li classList={{ selected: getSearchParams().by === 'shouts' }}>
|
||||
<li classList={{ selected: searchParams().by === 'shouts' }}>
|
||||
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
|
||||
{t('By shouts')}
|
||||
</a>
|
||||
</li>
|
||||
<li classList={{ selected: getSearchParams().by === 'rating' }}>
|
||||
<li classList={{ selected: searchParams().by === 'rating' }}>
|
||||
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
|
||||
{t('By rating')}
|
||||
</a>
|
||||
</li>
|
||||
<li classList={{ selected: !getSearchParams().by || getSearchParams().by === 'name' }}>
|
||||
<li classList={{ selected: !searchParams().by || searchParams().by === 'name' }}>
|
||||
<a href="/authors" onClick={handleClientRouteLinkClick}>
|
||||
{t('By alphabet')}
|
||||
</a>
|
||||
|
@ -92,7 +92,7 @@ export const AllAuthorsView = (props: Props) => {
|
|||
</li>
|
||||
</ul>
|
||||
<Show
|
||||
when={!getSearchParams().by || getSearchParams().by === 'name'}
|
||||
when={!searchParams().by || searchParams().by === 'name'}
|
||||
fallback={() => (
|
||||
<div class="stats">
|
||||
<For each={sortedAuthors()}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { createEffect, createMemo, For, Show } from 'solid-js'
|
|||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { Icon } from '../Nav/Icon'
|
||||
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 { TopicCard } from '../Topic/Card'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
@ -17,17 +17,17 @@ type AllTopicsViewProps = {
|
|||
}
|
||||
|
||||
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||
const { getSearchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||
|
||||
const { sortedTopics } = useTopicsStore({
|
||||
topics: props.topics,
|
||||
sortBy: getSearchParams().by || 'shouts'
|
||||
sortBy: searchParams().by || 'shouts'
|
||||
})
|
||||
|
||||
const { session } = useAuthStore()
|
||||
|
||||
createEffect(() => {
|
||||
setSortAllTopicsBy(getSearchParams().by || 'shouts')
|
||||
setTopicsSort(searchParams().by || 'shouts')
|
||||
})
|
||||
|
||||
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
||||
|
@ -66,17 +66,17 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<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}>
|
||||
{t('By shouts')}
|
||||
</a>
|
||||
</li>
|
||||
<li classList={{ selected: getSearchParams().by === 'authors' }}>
|
||||
<li classList={{ selected: searchParams().by === 'authors' }}>
|
||||
<a href="/topics?by=authors" onClick={handleClientRouteLinkClick}>
|
||||
{t('By authors')}
|
||||
</a>
|
||||
</li>
|
||||
<li classList={{ selected: getSearchParams().by === 'title' }}>
|
||||
<li classList={{ selected: searchParams().by === 'title' }}>
|
||||
<a
|
||||
href="/topics?by=title"
|
||||
onClick={(ev) => {
|
||||
|
@ -97,7 +97,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
</ul>
|
||||
|
||||
<Show
|
||||
when={getSearchParams().by === 'title'}
|
||||
when={searchParams().by === 'title'}
|
||||
fallback={() => (
|
||||
<div class="stats">
|
||||
<For each={sortedTopics()}>
|
||||
|
|
|
@ -34,10 +34,10 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
const { topicsByAuthor } = useTopicsStore()
|
||||
|
||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||
const { getSearchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||
|
||||
const title = createMemo(() => {
|
||||
const m = getSearchParams().by
|
||||
const m = searchParams().by
|
||||
if (m === 'viewed') return t('Top viewed')
|
||||
if (m === 'rating') return t('Top rated')
|
||||
if (m === 'commented') return t('Top discussed')
|
||||
|
@ -51,7 +51,7 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
<div class="row group__controls">
|
||||
<div class="col-md-8">
|
||||
<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')}>
|
||||
{t('Recent')}
|
||||
</button>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const SearchView = (props: Props) => {
|
|||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
||||
const [getQuery, setQuery] = createSignal(props.query)
|
||||
|
||||
const { getSearchParams } = useRouter<SearchPageSearchParams>()
|
||||
const { searchParams } = useRouter<SearchPageSearchParams>()
|
||||
|
||||
const handleQueryChange = (ev) => {
|
||||
setQuery(ev.target.value)
|
||||
|
@ -48,7 +48,7 @@ export const SearchView = (props: Props) => {
|
|||
<ul class="view-switcher">
|
||||
<li
|
||||
classList={{
|
||||
selected: getSearchParams().by === 'relevance'
|
||||
selected: searchParams().by === 'relevance'
|
||||
}}
|
||||
>
|
||||
<a href="?by=relevance" onClick={handleClientRouteLinkClick}>
|
||||
|
@ -57,7 +57,7 @@ export const SearchView = (props: Props) => {
|
|||
</li>
|
||||
<li
|
||||
classList={{
|
||||
selected: getSearchParams().by === 'rating'
|
||||
selected: searchParams().by === 'rating'
|
||||
}}
|
||||
>
|
||||
<a href="?by=rating" onClick={handleClientRouteLinkClick}>
|
||||
|
|
|
@ -23,7 +23,7 @@ interface TopicProps {
|
|||
}
|
||||
|
||||
export const TopicView = (props: TopicProps) => {
|
||||
const { getSearchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||
|
||||
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||
|
@ -33,7 +33,7 @@ export const TopicView = (props: TopicProps) => {
|
|||
const topic = createMemo(() => topicEntities()[props.topicSlug])
|
||||
|
||||
const title = createMemo(() => {
|
||||
const m = getSearchParams().by
|
||||
const m = searchParams().by
|
||||
if (m === 'viewed') return t('Top viewed')
|
||||
if (m === 'rating') return t('Top rated')
|
||||
if (m === 'commented') return t('Top discussed')
|
||||
|
@ -47,7 +47,7 @@ export const TopicView = (props: TopicProps) => {
|
|||
<div class="row group__controls">
|
||||
<div class="col-md-8">
|
||||
<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')}>
|
||||
{t('Recent')}
|
||||
</button>
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query ConfirmEmailQuery($code: String!) {
|
||||
mutation ConfirmEmailMutation($code: String!) {
|
||||
confirmEmail(code: $code) {
|
||||
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'
|
||||
|
||||
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) {
|
||||
error
|
||||
token
|
||||
user {
|
||||
_id: slug
|
||||
name
|
||||
slug
|
||||
userpic
|
||||
bio
|
||||
# links
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -15,9 +15,9 @@ export default gql`
|
|||
}
|
||||
news {
|
||||
unread
|
||||
inbox
|
||||
topics
|
||||
authors
|
||||
reactions
|
||||
communities
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createClient, ClientOptions, dedupExchange, fetchExchange, Exchange } from '@urql/core'
|
||||
import { devtoolsExchange } from '@urql/devtools'
|
||||
import { baseUrl } from './publicGraphQLClient'
|
||||
import { isDev } from '../utils/config'
|
||||
import { isDev, apiBaseUrl } from '../utils/config'
|
||||
|
||||
const TOKEN_LOCAL_STORAGE_KEY = 'token'
|
||||
|
||||
|
@ -20,7 +19,7 @@ export const resetToken = () => {
|
|||
}
|
||||
|
||||
const options: ClientOptions = {
|
||||
url: baseUrl,
|
||||
url: apiBaseUrl,
|
||||
maskTypename: true,
|
||||
requestPolicy: 'cache-and-network',
|
||||
fetchOptions: () => {
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { ClientOptions, dedupExchange, fetchExchange, createClient, Exchange } from '@urql/core'
|
||||
import { devtoolsExchange } from '@urql/devtools'
|
||||
import { isDev } from '../utils/config'
|
||||
|
||||
export const baseUrl = 'https://newapi.discours.io'
|
||||
// export const baseUrl = 'http://localhost:8000'
|
||||
import { isDev, apiBaseUrl } from '../utils/config'
|
||||
|
||||
const exchanges: Exchange[] = [dedupExchange, fetchExchange]
|
||||
|
||||
|
@ -12,7 +9,7 @@ if (isDev) {
|
|||
}
|
||||
|
||||
const options: ClientOptions = {
|
||||
url: baseUrl,
|
||||
url: apiBaseUrl,
|
||||
maskTypename: true,
|
||||
requestPolicy: 'cache-and-network',
|
||||
exchanges
|
||||
|
|
|
@ -260,8 +260,8 @@ export type MutationRateUserArgs = {
|
|||
|
||||
export type MutationRegisterUserArgs = {
|
||||
email: Scalars['String']
|
||||
password?: InputMaybe<Scalars['String']>
|
||||
name?: InputMaybe<Scalars['String']>
|
||||
password?: InputMaybe<Scalars['String']>
|
||||
}
|
||||
|
||||
export type MutationRemoveAuthorArgs = {
|
||||
|
@ -271,6 +271,7 @@ export type MutationRemoveAuthorArgs = {
|
|||
|
||||
export type MutationSendLinkArgs = {
|
||||
email: Scalars['String']
|
||||
lang?: InputMaybe<Scalars['String']>
|
||||
}
|
||||
|
||||
export type MutationUnfollowArgs = {
|
||||
|
@ -489,6 +490,7 @@ export type QueryShoutsForFeedArgs = {
|
|||
|
||||
export type QuerySignInArgs = {
|
||||
email: Scalars['String']
|
||||
lang?: InputMaybe<Scalars['String']>
|
||||
password?: InputMaybe<Scalars['String']>
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
"collections": "коллекции",
|
||||
"community": "сообщество",
|
||||
"email not confirmed": "email не подтвержден",
|
||||
"enter": "войти",
|
||||
"Enter": "Войти",
|
||||
"feed": "лента",
|
||||
"follower": "подписчик",
|
||||
"invalid password": "некорректный пароль",
|
||||
|
@ -147,22 +147,16 @@
|
|||
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
|
||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||
"You was successfully authorized": "Вы были успешно авторизованы",
|
||||
"Help discours to grow": "Поддержка дискурса",
|
||||
"One time": "Единоразово",
|
||||
"Every month": "Ежемесячно",
|
||||
"Another amount": "Другая сумма",
|
||||
"Just start typing...": "Просто начните...",
|
||||
"Tips and proposals": "Советы и предложения",
|
||||
"Invite coauthors": "Пригласить соавторов",
|
||||
"Tabula rasa": "С чистого листа",
|
||||
"Publication settings": "Настройки публикации",
|
||||
"History of changes": "История правок",
|
||||
"Undo": "Откат",
|
||||
"Redo": "Повторить действие",
|
||||
"Stop collab": "Индивидуальный режим",
|
||||
"Restart collab": "Перезапустить коллаборацию",
|
||||
"Start collab": "Коллаборативный режим",
|
||||
"Clear": "Сбросить",
|
||||
"Theme": "Режим",
|
||||
"Editing conflict, please copy your version and refresh page": "Конфликт редактирования, пожалуйста, скопируйте вашу версию текста и обновите страницу"
|
||||
"Invalid email": "Проверьте правильность ввода почты",
|
||||
"Please enter email": "Пожалуйста, введите почту",
|
||||
"Please enter password": "Пожалуйста, введите пароль",
|
||||
"Please enter password again": "Пожалуйста, введите пароль ещё рез",
|
||||
"Join": "Присоединиться",
|
||||
"Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте",
|
||||
"Full name": "Имя и фамилия",
|
||||
"Restore password": "Восстановить пароль",
|
||||
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||
"You've confirmed email": "Вы подтвердили почту",
|
||||
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
||||
"enter": "войдите"
|
||||
}
|
||||
|
|
|
@ -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 { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
|
@ -13,37 +12,46 @@ export const signIn = async (params) => {
|
|||
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 = () => {
|
||||
// TODO: call backend to revoke token
|
||||
setSession(null)
|
||||
resetToken()
|
||||
console.debug('signed out')
|
||||
}
|
||||
|
||||
export const emailChecks = atom<{ [email: string]: boolean }>({})
|
||||
export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
||||
|
||||
export const signCheck = async (params) => {
|
||||
emailChecks.set(await apiClient.authCheckEmail(params))
|
||||
export const checkEmail = async (email: string): Promise<boolean> => {
|
||||
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 }) => {
|
||||
const authResult = await apiClient.authRegister({
|
||||
export const register = async ({
|
||||
name,
|
||||
email,
|
||||
password
|
||||
}: {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
}) => {
|
||||
await apiClient.authRegister({
|
||||
name,
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
if (authResult && !authResult.error) {
|
||||
console.debug('register session update', authResult)
|
||||
setSession(authResult)
|
||||
}
|
||||
}
|
||||
|
||||
export const signSendLink = async (params) => {
|
||||
|
@ -51,18 +59,24 @@ export const signSendLink = async (params) => {
|
|||
resetToken()
|
||||
}
|
||||
|
||||
export const signConfirm = async (params) => {
|
||||
const auth = await apiClient.authConfirmCode(params) // { code }
|
||||
setToken(auth.token)
|
||||
setSession(auth)
|
||||
}
|
||||
|
||||
export const renewSession = async () => {
|
||||
const authResult = await apiClient.getSession() // token in header
|
||||
setToken(authResult.token)
|
||||
setSession(authResult)
|
||||
}
|
||||
|
||||
export const useAuthStore = () => {
|
||||
return { session }
|
||||
export const confirmEmail = async (token: string) => {
|
||||
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)
|
||||
if (url.origin === location.origin) {
|
||||
event.preventDefault()
|
||||
// TODO: search params
|
||||
|
||||
routerStore.open(url.pathname)
|
||||
const params = Object.fromEntries(new URLSearchParams(url.search))
|
||||
searchParamsStore.open(params)
|
||||
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0
|
||||
|
@ -91,16 +94,27 @@ if (!isServer) {
|
|||
}
|
||||
|
||||
export const useRouter = <TSearchParams extends Record<string, string> = Record<string, string>>() => {
|
||||
const getPage = useStore(routerStore)
|
||||
const getSearchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
|
||||
const page = useStore(routerStore)
|
||||
const searchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
|
||||
|
||||
const changeSearchParam = <TKey extends keyof TSearchParams>(key: TKey, value: TSearchParams[TKey]) => {
|
||||
searchParamsStore.open({ ...searchParamsStore.get(), [key]: value })
|
||||
const changeSearchParam = <TKey extends keyof TSearchParams>(
|
||||
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 {
|
||||
getPage,
|
||||
getSearchParams,
|
||||
page,
|
||||
searchParams,
|
||||
changeSearchParam
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
//import { persistentAtom } from '@nanostores/persistent'
|
||||
import { atom } from 'nanostores'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { useRouter } from './router'
|
||||
|
||||
//export const locale = persistentAtom<string>('locale', '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'
|
||||
|
||||
export interface Warning {
|
||||
|
@ -14,28 +13,38 @@ export interface Warning {
|
|||
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)
|
||||
export const hideModal = () => modal.set(null)
|
||||
export const toggleModal = (modalType) => modal.get() ? hideModal() : showModal(modalType)
|
||||
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
||||
|
||||
export const clearWarns = () => warnings.set([])
|
||||
export const warn = (warning: Warning) => warnings.set([...warnings.get(), warning])
|
||||
export const showModal = (modalType: ModalType) => setModal(modalType)
|
||||
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 = () => {
|
||||
const getWarnings = useStore(warnings)
|
||||
return {
|
||||
getWarnings
|
||||
warnings
|
||||
}
|
||||
}
|
||||
|
||||
export const useModalStore = () => {
|
||||
const getModal = useStore(modal)
|
||||
|
||||
return {
|
||||
getModal
|
||||
modal
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ export type AuthorsSortBy = 'shouts' | 'name' | 'rating'
|
|||
|
||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
|
||||
|
||||
export { setSortAllBy }
|
||||
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||
|
||||
const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: 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')
|
||||
|
||||
export { setSortAllBy }
|
||||
export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy)
|
||||
|
||||
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
--danger-color: #fc6847;
|
||||
--lightgray-color: rgb(84 16 17 / 6%);
|
||||
--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,
|
||||
textarea {
|
||||
&:focus,
|
||||
&:-webkit-autofill,
|
||||
&:not(:placeholder-shown) {
|
||||
& ~ label {
|
||||
font-size: 60%;
|
||||
|
|
|
@ -11,7 +11,7 @@ import authLogoutQuery from '../graphql/mutation/auth-logout'
|
|||
import authLoginQuery from '../graphql/query/auth-login'
|
||||
import authRegisterMutation from '../graphql/mutation/auth-register'
|
||||
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 followMutation from '../graphql/mutation/follow'
|
||||
import unfollowMutation from '../graphql/mutation/unfollow'
|
||||
|
@ -32,7 +32,7 @@ import myChats from '../graphql/query/my-chats'
|
|||
const FEED_SIZE = 50
|
||||
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 {
|
||||
code: ApiErrorCode
|
||||
|
@ -65,12 +65,26 @@ export const apiClient = {
|
|||
|
||||
return response.data.signIn
|
||||
},
|
||||
authRegister: async ({ email, password = '', name = '' }): Promise<AuthResult> => {
|
||||
// NOTE: name is to display
|
||||
authRegister: async ({
|
||||
email,
|
||||
password,
|
||||
name
|
||||
}: {
|
||||
email: string
|
||||
password: string
|
||||
name: string
|
||||
}): Promise<void> => {
|
||||
const response = await publicGraphQLClient
|
||||
.mutation(authRegisterMutation, { email, password, name })
|
||||
.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 () => {
|
||||
const response = await publicGraphQLClient.query(authLogoutQuery, {}).toPromise()
|
||||
|
@ -79,6 +93,7 @@ export const apiClient = {
|
|||
authCheckEmail: async ({ email }) => {
|
||||
// check if email is used
|
||||
const response = await publicGraphQLClient.query(authCheckEmailQuery, { email }).toPromise()
|
||||
log.debug('authCheckEmail', response)
|
||||
return response.data.isEmailUsed
|
||||
},
|
||||
authSendLink: async ({ email }) => {
|
||||
|
@ -86,10 +101,21 @@ export const apiClient = {
|
|||
const response = await publicGraphQLClient.query(authSendLinkMutation, { email }).toPromise()
|
||||
return response.data.reset
|
||||
},
|
||||
authConfirmCode: async ({ code }) => {
|
||||
confirmEmail: async ({ token }: { token: string }) => {
|
||||
// confirm email with code from link
|
||||
const response = await publicGraphQLClient.query(authConfirmCodeMutation, { code }).toPromise()
|
||||
return response.data.reset
|
||||
const response = await publicGraphQLClient
|
||||
.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 () => {
|
||||
|
@ -216,6 +242,16 @@ export const apiClient = {
|
|||
getSession: async (): Promise<AuthResult> => {
|
||||
// renew session with auth token in header (!)
|
||||
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
|
||||
},
|
||||
getPublishedArticles: async ({ limit = FEED_SIZE, offset }: { limit?: number; offset?: number }) => {
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
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