From 3374e9677a91d179b49a61640b288ca791d35efa Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 21 Oct 2022 16:24:24 +0300 Subject: [PATCH 1/2] Rewrite banner component to css module --- .../Discours/{Banner.scss => Banner.module.scss} | 10 +++------- src/components/Discours/Banner.tsx | 9 +++++---- 2 files changed, 8 insertions(+), 11 deletions(-) rename src/components/Discours/{Banner.scss => Banner.module.scss} (82%) diff --git a/src/components/Discours/Banner.scss b/src/components/Discours/Banner.module.scss similarity index 82% rename from src/components/Discours/Banner.scss rename to src/components/Discours/Banner.module.scss index 36557922..5a8bfc89 100644 --- a/src/components/Discours/Banner.scss +++ b/src/components/Discours/Banner.module.scss @@ -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; } diff --git a/src/components/Discours/Banner.tsx b/src/components/Discours/Banner.tsx index 22438dd7..17c8875a 100644 --- a/src/components/Discours/Banner.tsx +++ b/src/components/Discours/Banner.tsx @@ -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 ( -
+
-
+

{t('Discours is created with our common effort')}

{t('Support us')} @@ -16,7 +17,7 @@ export default () => {

-
+
{t('Discours')}
From 4fabc7554dd3a8a03ebe0c8227c38217ad5eb306 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Fri, 21 Oct 2022 20:17:04 +0200 Subject: [PATCH 2/2] WIP --- src/components/Author/Card.module.scss | 1 + src/components/Nav/AuthModal.tsx | 356 ------------------ .../AuthModal.module.scss} | 113 ++---- .../Nav/AuthModal/EmailConfirm.module.scss | 13 + src/components/Nav/AuthModal/EmailConfirm.tsx | 41 ++ .../Nav/AuthModal/ForgotPasswordForm.tsx | 100 +++++ src/components/Nav/AuthModal/LoginForm.tsx | 150 ++++++++ src/components/Nav/AuthModal/RegisterForm.tsx | 189 ++++++++++ .../Nav/AuthModal/SocialProviders.module.scss | 45 +++ .../Nav/AuthModal/SocialProviders.tsx | 42 +++ src/components/Nav/AuthModal/index.tsx | 88 +++++ src/components/Nav/AuthModal/sharedLogic.tsx | 5 + src/components/Nav/AuthModal/types.ts | 5 + src/components/Nav/AuthModal/validators.ts | 7 + src/components/Nav/Header.tsx | 51 +-- src/components/Nav/Modal.tsx | 26 +- src/components/Nav/Notifications.tsx | 6 +- src/components/Nav/Popup.tsx | 10 +- src/components/Nav/Private.tsx | 6 +- src/components/Pages/ArticlePage.tsx | 2 +- src/components/Pages/AuthorPage.tsx | 2 +- src/components/Pages/SearchPage.tsx | 2 +- src/components/Pages/TopicPage.tsx | 2 +- src/components/Root.tsx | 26 +- src/components/Views/AllAuthors.tsx | 15 +- src/components/Views/AllTopics.tsx | 16 +- src/components/Views/Author.tsx | 6 +- src/components/Views/Search.tsx | 6 +- src/components/Views/Topic.tsx | 6 +- src/graphql/mutation/auth-confirm-email.ts | 19 +- src/graphql/mutation/auth-register.ts | 13 +- src/graphql/mutation/my-session.ts | 2 +- src/graphql/privateGraphQLClient.ts | 5 +- src/graphql/publicGraphQLClient.ts | 7 +- src/graphql/types.gen.ts | 4 +- src/locales/ru.json | 16 +- src/pages/api/sendlink.ts | 31 -- src/pages/welcome.astro | 3 + src/stores/auth.ts | 62 +-- src/stores/router.ts | 28 +- src/stores/ui.ts | 39 +- src/stores/zine/authors.ts | 2 +- src/stores/zine/topics.ts | 2 +- src/styles/app.scss | 3 +- src/utils/apiClient.ts | 53 ++- src/utils/config.ts | 3 + src/utils/validators.ts | 29 -- 47 files changed, 1000 insertions(+), 658 deletions(-) delete mode 100644 src/components/Nav/AuthModal.tsx rename src/components/Nav/{AuthModal.scss => AuthModal/AuthModal.module.scss} (62%) create mode 100644 src/components/Nav/AuthModal/EmailConfirm.module.scss create mode 100644 src/components/Nav/AuthModal/EmailConfirm.tsx create mode 100644 src/components/Nav/AuthModal/ForgotPasswordForm.tsx create mode 100644 src/components/Nav/AuthModal/LoginForm.tsx create mode 100644 src/components/Nav/AuthModal/RegisterForm.tsx create mode 100644 src/components/Nav/AuthModal/SocialProviders.module.scss create mode 100644 src/components/Nav/AuthModal/SocialProviders.tsx create mode 100644 src/components/Nav/AuthModal/index.tsx create mode 100644 src/components/Nav/AuthModal/sharedLogic.tsx create mode 100644 src/components/Nav/AuthModal/types.ts create mode 100644 src/components/Nav/AuthModal/validators.ts delete mode 100644 src/pages/api/sendlink.ts create mode 100644 src/pages/welcome.astro delete mode 100644 src/utils/validators.ts diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index b0b4bb2c..1dece175 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -97,6 +97,7 @@ } } + a[href*='vk.cc/'], a[href*='vk.com/'] { &::before { background-image: url(/icons/vk-white.svg); diff --git a/src/components/Nav/AuthModal.tsx b/src/components/Nav/AuthModal.tsx deleted file mode 100644 index e26ad6b2..00000000 --- a/src/components/Nav/AuthModal.tsx +++ /dev/null @@ -1,356 +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('') - const [mode, setMode] = createSignal('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 ( -
-
-
-

{t('Discours')}

-

{t(`Join the global community of authors!`)}

-

- {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!')} -

-

- {t('By signing up you agree with our')}{' '} - { - hideModal() - handleClientRouteLinkClick(event) - }} - > - {t('terms of use')} - - , {t('personal data usage and email notifications')}. -

-
-
-
-
{ - console.log('[auth] form values', form.values) - }} - > -
-

{titles[mode()]}

-
- - {t('Enter the code or click the link from email to confirm')} - - } - > - {t('Everything is ok, please give us your email address')} - -
- -
-
    -
  • {authError()}
  • -
-
-
- {/*FIXME*/} - {/**/} - {/*
*/} - {/* */} - {/* */} - {/*
*/} - {/*
*/} - -
- - -
-
- -
- - -
-
- -
- - -
-
- -
- - -
-
-
- -
- - - - - - -
-
- changeMode('sign-in')}> - {t('I have an account')} - -
-
- changeMode('sign-up')}> - {t('I have no account yet')} - -
-
- changeMode('sign-in')}> - {t('I know the password')} - -
-
- changeMode('resend')}> - {t('Resend code')} - -
-
-
-
-
-
- ) -} diff --git a/src/components/Nav/AuthModal.scss b/src/components/Nav/AuthModal/AuthModal.module.scss similarity index 62% rename from src/components/Nav/AuthModal.scss rename to src/components/Nav/AuthModal/AuthModal.module.scss index 13b66e81..7df0e4dc 100644 --- a/src/components/Nav/AuthModal.scss +++ b/src/components/Nav/AuthModal/AuthModal.module.scss @@ -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; + } } } diff --git a/src/components/Nav/AuthModal/EmailConfirm.module.scss b/src/components/Nav/AuthModal/EmailConfirm.module.scss new file mode 100644 index 00000000..323b5384 --- /dev/null +++ b/src/components/Nav/AuthModal/EmailConfirm.module.scss @@ -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; +} diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx new file mode 100644 index 00000000..6588285e --- /dev/null +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -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() + + onMount(async () => { + const token = searchParams().token + try { + await confirmEmail(token) + } catch (error) { + console.log(error) + } + }) + + return ( +
+
{t('Hooray! Welcome!')}
+
+ {t("You've confirmed email")} {confirmedEmail} +
+
+ +
+
+ ) +} diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx new file mode 100644 index 00000000..ebd09d47 --- /dev/null +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -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> + +export const ForgotPasswordForm = () => { + const { changeSearchParam } = useRouter() + + const handleEmailInput = (newEmail: string) => { + setValidationErrors(({ email: _notNeeded, ...rest }) => rest) + setEmail(newEmail) + } + + const [submitError, setSubmitError] = createSignal('') + const [isSubmitting, setIsSubmitting] = createSignal(false) + const [validationErrors, setValidationErrors] = createSignal({}) + + 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 ( +
+

{t('Forgot password?')}

+ {t('Everything is ok, please give us your email address')} + +
+
    +
  • {submitError()}
  • +
+
+
+
+ handleEmailInput(event.currentTarget.value)} + /> + + +
+ +
+ +
+
+ changeSearchParam('mode', 'login')}> + {t('I know the password')} + +
+
+ ) +} diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx new file mode 100644 index 00000000..115ee075 --- /dev/null +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -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> + +export const LoginForm = () => { + const [submitError, setSubmitError] = createSignal('') + const [isSubmitting, setIsSubmitting] = createSignal(false) + const [validationErrors, setValidationErrors] = createSignal({}) + + const { changeSearchParam } = useRouter() + + 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 ( +
+

{t('Enter the Discours')}

+ +
+
    +
  • {submitError()}
  • +
+
+
+
+ handleEmailInput(event.currentTarget.value)} + /> + +
+ +
{validationErrors().email}
+
+
+ handlePasswordInput(event.currentTarget.value)} + /> + +
+ +
{validationErrors().password}
+
+
+ +
+ + + + +
+ changeSearchParam('mode', 'register')}> + {t('I have no account yet')} + +
+ + ) +} diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx new file mode 100644 index 00000000..b7a581b6 --- /dev/null +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -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> + +export const RegisterForm = () => { + const { changeSearchParam } = useRouter() + + const { emailChecks } = useAuthStore() + + const [submitError, setSubmitError] = createSignal('') + const [name, setName] = createSignal('') + const [password, setPassword] = createSignal('') + const [isSubmitting, setIsSubmitting] = createSignal(false) + const [validationErrors, setValidationErrors] = createSignal({}) + + 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 ( +
+

{t('Create account')}

+ +
+
    +
  • {submitError()}
  • +
+
+
+
+ handleNameInput(event.currentTarget.value)} + /> + +
+ +
{validationErrors().name}
+
+
+ handleEmailInput(event.currentTarget.value)} + onBlur={handleEmailBlur} + /> + +
+ +
{validationErrors().email}
+
+ + + +
+ handlePasswordInput(event.currentTarget.value)} + /> + +
+ +
{validationErrors().password}
+
+ +
+ +
+ + + +
+ changeSearchParam('mode', 'login')}> + {t('I have an account')} + +
+ + ) +} diff --git a/src/components/Nav/AuthModal/SocialProviders.module.scss b/src/components/Nav/AuthModal/SocialProviders.module.scss new file mode 100644 index 00000000..b16a0b3a --- /dev/null +++ b/src/components/Nav/AuthModal/SocialProviders.module.scss @@ -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); + } + } +} diff --git a/src/components/Nav/AuthModal/SocialProviders.tsx b/src/components/Nav/AuthModal/SocialProviders.tsx new file mode 100644 index 00000000..1daefd67 --- /dev/null +++ b/src/components/Nav/AuthModal/SocialProviders.tsx @@ -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 ( + + ) +} diff --git a/src/components/Nav/AuthModal/index.tsx b/src/components/Nav/AuthModal/index.tsx new file mode 100644 index 00000000..91480993 --- /dev/null +++ b/src/components/Nav/AuthModal/index.tsx @@ -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 = { + 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() + + const mode = createMemo(() => { + return AUTH_MODAL_MODES[searchParams().mode] || 'login' + }) + + createEffect((oldMode) => { + if (oldMode !== mode()) { + rootRef?.querySelector('input')?.focus() + } + }, null) + + return ( +
+
+
+

{t('Discours')}

+

{t(`Join the global community of authors!`)}

+

+ {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!')} +

+

+ {t('By signing up you agree with our')}{' '} + { + hideModal() + handleClientRouteLinkClick(event) + }} + > + {t('terms of use')} + + , {t('personal data usage and email notifications')}. +

+
+
+
+ + + + + + + + + + + + +
+
+ ) +} diff --git a/src/components/Nav/AuthModal/sharedLogic.tsx b/src/components/Nav/AuthModal/sharedLogic.tsx new file mode 100644 index 00000000..1d956b0e --- /dev/null +++ b/src/components/Nav/AuthModal/sharedLogic.tsx @@ -0,0 +1,5 @@ +import { createSignal } from 'solid-js' + +const [email, setEmail] = createSignal('') + +export { email, setEmail } diff --git a/src/components/Nav/AuthModal/types.ts b/src/components/Nav/AuthModal/types.ts new file mode 100644 index 00000000..e7aeed0e --- /dev/null +++ b/src/components/Nav/AuthModal/types.ts @@ -0,0 +1,5 @@ +export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password' + +export type AuthModalSearchParams = { + mode: AuthModalMode +} diff --git a/src/components/Nav/AuthModal/validators.ts b/src/components/Nav/AuthModal/validators.ts new file mode 100644 index 00000000..a4950dff --- /dev/null +++ b/src/components/Nav/AuthModal/validators.ts @@ -0,0 +1,7 @@ +export const isValidEmail = (email: string) => { + if (!email) { + return false + } + + return email.includes('@') && email.includes('.') && email.length > 5 +} diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index ab2d6aeb..043bfcf8 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -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' @@ -24,10 +24,6 @@ const resources: { name: string; route: keyof Routes }[] = [ { name: t('topics'), route: 'topics' } ] -const handleEnterClick = () => { - showModal('auth') -} - type Props = { title?: string isHeaderFixed?: boolean @@ -40,27 +36,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 @@ -103,31 +101,31 @@ export const Header = (props: Props) => {