From 44b4d789aee769e6d3915462aa7d1c3ca2d14af1 Mon Sep 17 00:00:00 2001 From: Vitaliy Prachov Date: Fri, 29 Oct 2021 13:24:01 +0300 Subject: [PATCH] feat: add Enroll page (link handling) --- front/src/i18n.js | 1 + front/src/pages/Invite/Invite.jsx | 94 ++++++++++++++++++ front/src/pages/Invite/Invite.styled.jsx | 66 +++++++++++++ front/src/pages/Invite/SignUp/SignUp.jsx | 97 +++++++++++++++++++ front/src/pages/Invite/SignUp/index.js | 3 + front/src/pages/Invite/index.js | 3 + front/src/pages/Invite/useForm.js | 60 ++++++++++++ front/src/resources/lang/en/index.js | 2 + front/src/resources/lang/en/invite.js | 8 ++ front/src/resources/lang/ru/index.js | 2 + front/src/resources/lang/ru/invite.js | 8 ++ .../PrivateRoutes/PrivateRoutes.utils.jsx | 1 + front/src/routes/Routes.jsx | 4 + front/src/utils/api/v1/invite.js | 42 ++++++++ front/src/utils/paths.js | 1 + front/src/utils/queries.js | 2 + 16 files changed, 394 insertions(+) create mode 100644 front/src/pages/Invite/Invite.jsx create mode 100644 front/src/pages/Invite/Invite.styled.jsx create mode 100644 front/src/pages/Invite/SignUp/SignUp.jsx create mode 100644 front/src/pages/Invite/SignUp/index.js create mode 100644 front/src/pages/Invite/index.js create mode 100644 front/src/pages/Invite/useForm.js create mode 100644 front/src/resources/lang/en/invite.js create mode 100644 front/src/resources/lang/ru/invite.js create mode 100644 front/src/utils/api/v1/invite.js diff --git a/front/src/i18n.js b/front/src/i18n.js index 3f9990a2..1d8dab38 100644 --- a/front/src/i18n.js +++ b/front/src/i18n.js @@ -64,6 +64,7 @@ i18n.use(initReactI18next).init({ 'change_password', 'forgot_password', 'verify_email', + 'invite', ], defaultNS: 'common', keySeparator: '.', diff --git a/front/src/pages/Invite/Invite.jsx b/front/src/pages/Invite/Invite.jsx new file mode 100644 index 00000000..d91f0599 --- /dev/null +++ b/front/src/pages/Invite/Invite.jsx @@ -0,0 +1,94 @@ +import { Row, Skeleton } from 'antd'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; +import { useParams } from 'react-router-dom'; + +import LessonKeywords from '@sb-ui/components/atoms/LessonKeywords'; +import * as SM from '@sb-ui/pages/User/EnrollCourseModal/EnrollModal.mobile.styled'; +import { getInvite } from '@sb-ui/utils/api/v1/invite'; +import { INVITE_LESSON_QUERY } from '@sb-ui/utils/queries'; + +import SignUp from './SignUp'; +import * as S from './Invite.styled'; + +const Invite = () => { + const { id } = useParams(); + const { t } = useTranslation('invite'); + + const { data: responseData, isLoading } = useQuery( + [INVITE_LESSON_QUERY, { id }], + getInvite, + ); + + const { lesson, keywords, email, isRegistered, inviteUser } = + responseData || {}; + + const { name, description, author, image } = lesson || {}; + + const fullName = useMemo( + () => `${author?.firstName} ${author?.lastName}`.trim(), + [author], + ); + + const firstNameLetter = useMemo( + () => author?.firstName?.[0] || author?.lastName?.[0], + [author], + ); + + const isAuth = false; + + return ( + + + + {isLoading ? ( + + ) : ( + <> +
+ +
+ + {t('header', { inviteUser, email })} + + + )} +
+ + + {isLoading ? ( + + ) : ( + <> + + + {firstNameLetter} + {fullName} + + + )} + + + {name} + + + {description} + {keywords && ( + + + + )} + + {isAuth ? ( + {t('buttons.join')} + ) : ( + + )} + +
+
+ ); +}; + +export default Invite; diff --git a/front/src/pages/Invite/Invite.styled.jsx b/front/src/pages/Invite/Invite.styled.jsx new file mode 100644 index 00000000..8db8dbd6 --- /dev/null +++ b/front/src/pages/Invite/Invite.styled.jsx @@ -0,0 +1,66 @@ +import { + Avatar as AvatarAntd, + Button as ButtonAntd, + Form as FormAntd, + Input, +} from 'antd'; +import styled from 'styled-components'; +import { UserOutlined } from '@ant-design/icons'; + +export const Page = styled.div` + display: flex; + justify-content: center; +`; + +export const Container = styled.div` + max-width: 500px; + width: 100%; +`; + +export const Button = styled(ButtonAntd).attrs({ + type: 'primary', + htmlType: 'submit', +})` + width: 100%; +`; + +export const JoinButton = styled(ButtonAntd).attrs({ + type: 'primary', +})` + margin-top: 1rem; + width: 100%; +`; + +export const Form = styled(FormAntd).attrs({ + layout: 'vertical', + size: 'large', +})` + margin-top: 1rem; +`; + +export const HeaderBlock = styled.div` + background-color: white; + padding: 1rem; + margin-bottom: 1rem; + display: flex; + justify-content: flex-start; + align-items: center; +`; + +export const HeaderTitle = styled.div` + margin-left: 1rem; +`; + +export const Avatar = styled(AvatarAntd).attrs({ + size: 'large', + icon: , +})``; + +export const BodyBlock = styled.div` + background-color: white; + padding: 2rem; +`; + +export const EmailInput = styled(Input)` + margin-bottom: 1.5rem; +`; diff --git a/front/src/pages/Invite/SignUp/SignUp.jsx b/front/src/pages/Invite/SignUp/SignUp.jsx new file mode 100644 index 00000000..cc3fcaab --- /dev/null +++ b/front/src/pages/Invite/SignUp/SignUp.jsx @@ -0,0 +1,97 @@ +import { Alert, Form, Input } from 'antd'; +import T from 'prop-types'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import PasswordStrengthIndicator from '@sb-ui/components/atoms/PasswordStrengthIndicator'; +import { useAuthentication } from '@sb-ui/hooks/useAuthentication'; +import { useForm } from '@sb-ui/pages/Invite/useForm'; +import { postSignUp } from '@sb-ui/utils/api/v1/user'; + +import * as S from '../Invite.styled'; + +const SignUp = ({ email, isRegistered }) => { + const { t } = useTranslation('sign_up'); + const [form] = Form.useForm(); + + const [isFormErrors, setIsFormErrors] = useState(false); + + const handleFieldsChange = useCallback(() => { + setIsFormErrors(form.getFieldsError().some(({ errors }) => errors.length)); + }, [form]); + + const { formRules, password } = useForm({ isRegistered }); + + const handleSubmit = async () => { + // TODO: make here implementation after connecting with API; + }; + + // TODO: remove it after connecting with API; + // eslint-disable-next-line no-unused-vars + const [auth, error, setError] = useAuthentication({ + requestFunc: postSignUp, + message: 'sign_up:email_verification', + }); + + const loading = false; + + const buttonKey = isRegistered + ? 'invite:buttons.sign_in' + : 'invite:buttons.sign_up'; + + return ( + + {error && ( + + setError(null)} + message={t(error)} + type="error" + showIcon + closable + /> + + )} + + + + {!isRegistered && ( + <> + + + + + + + + + )} + + +
+ + {!isRegistered && } +
+
+ + + {t(buttonKey)} + +
+ ); +}; + +SignUp.propTypes = { + email: T.string, + isRegistered: T.bool, +}; + +export default SignUp; diff --git a/front/src/pages/Invite/SignUp/index.js b/front/src/pages/Invite/SignUp/index.js new file mode 100644 index 00000000..2698b4ab --- /dev/null +++ b/front/src/pages/Invite/SignUp/index.js @@ -0,0 +1,3 @@ +import SignUp from './SignUp'; + +export default SignUp; diff --git a/front/src/pages/Invite/index.js b/front/src/pages/Invite/index.js new file mode 100644 index 00000000..776f09a4 --- /dev/null +++ b/front/src/pages/Invite/index.js @@ -0,0 +1,3 @@ +import Invite from './Invite'; + +export default Invite; diff --git a/front/src/pages/Invite/useForm.js b/front/src/pages/Invite/useForm.js new file mode 100644 index 00000000..954c5674 --- /dev/null +++ b/front/src/pages/Invite/useForm.js @@ -0,0 +1,60 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { usePasswordInput } from '@sb-ui/hooks/usePasswordInput'; + +export const useForm = ({ isRegistered }) => { + const { password, passwordValidator } = usePasswordInput(); + const { t } = useTranslation('sign_up'); + + const additionalRules = useMemo( + () => + isRegistered + ? {} + : { + firstName: [ + { + required: true, + message: t('first_name.error'), + }, + ], + lastName: [ + { + required: true, + message: t('last_name.error'), + }, + ], + }, + [isRegistered, t], + ); + + const formRules = useMemo( + () => ({ + email: [ + { + required: true, + message: t('email.error'), + }, + { + type: 'email', + message: t('email.validation'), + }, + ], + ...additionalRules, + password: [ + { + required: true, + message: t('password.error'), + }, + isRegistered + ? {} + : { + validator: passwordValidator, + }, + ], + }), + [t, additionalRules, isRegistered, passwordValidator], + ); + + return { formRules, password }; +}; diff --git a/front/src/resources/lang/en/index.js b/front/src/resources/lang/en/index.js index b720414b..64612783 100644 --- a/front/src/resources/lang/en/index.js +++ b/front/src/resources/lang/en/index.js @@ -5,6 +5,7 @@ import common from './common'; import editorjs from './editorjs'; import email from './email'; import forgot_password from './forgot_password'; +import invite from './invite'; import profile from './profile'; import sign_in from './sign_in'; import sign_up from './sign_up'; @@ -24,5 +25,6 @@ export default { editorjs, email, forgot_password, + invite, verify_email, }; diff --git a/front/src/resources/lang/en/invite.js b/front/src/resources/lang/en/invite.js new file mode 100644 index 00000000..6e4d7306 --- /dev/null +++ b/front/src/resources/lang/en/invite.js @@ -0,0 +1,8 @@ +export default { + buttons: { + sign_up: 'Create an account & Enroll', + sign_in: 'Sign in & Enroll', + join: 'Join', + }, + header: '{{inviteUser}} invite you ({{email}}) to join the lesson', +}; diff --git a/front/src/resources/lang/ru/index.js b/front/src/resources/lang/ru/index.js index 9311ddf3..f5c3673f 100644 --- a/front/src/resources/lang/ru/index.js +++ b/front/src/resources/lang/ru/index.js @@ -5,6 +5,7 @@ import common from './common'; import editorjs from './editorjs'; import email from './email'; import forgot_password from './forgot_password'; +import invite from './invite'; import profile from './profile'; import sign_in from './sign_in'; import sign_up from './sign_up'; @@ -24,5 +25,6 @@ export default { editorjs, email, forgot_password, + invite, verify_email, }; diff --git a/front/src/resources/lang/ru/invite.js b/front/src/resources/lang/ru/invite.js new file mode 100644 index 00000000..c631e8c3 --- /dev/null +++ b/front/src/resources/lang/ru/invite.js @@ -0,0 +1,8 @@ +export default { + buttons: { + sign_up: 'Создать аккаунт & Записаться', + sign_in: 'Ввойти & Записаться', + join: 'Присоединиться', + }, + header: '{{inviteUser}} пригласил вас ({{email}}) на урок', +}; diff --git a/front/src/routes/PrivateRoutes/PrivateRoutes.utils.jsx b/front/src/routes/PrivateRoutes/PrivateRoutes.utils.jsx index 0e3a6553..306a4b8e 100644 --- a/front/src/routes/PrivateRoutes/PrivateRoutes.utils.jsx +++ b/front/src/routes/PrivateRoutes/PrivateRoutes.utils.jsx @@ -47,6 +47,7 @@ export const getPrivateRoutes = ({ isMobile }) => [ permissions: [Roles.SUPER_ADMIN], exact: true, }, + { component: Profile, path: paths.PROFILE, diff --git a/front/src/routes/Routes.jsx b/front/src/routes/Routes.jsx index 255b2a64..57cc6402 100644 --- a/front/src/routes/Routes.jsx +++ b/front/src/routes/Routes.jsx @@ -3,6 +3,7 @@ import { BrowserRouter as Router, Switch } from 'react-router-dom'; import ChangePassword from '../pages/ChangePassword'; import EmailSent from '../pages/EmailSent'; import ForgotPassword from '../pages/ForgotPassword'; +import Invite from '../pages/Invite'; import SignIn from '../pages/SignIn'; import SignUp from '../pages/SignUp'; import VerifyEmail from '../pages/VerifyEmail'; @@ -26,6 +27,9 @@ const Routes = () => ( + + + diff --git a/front/src/utils/api/v1/invite.js b/front/src/utils/api/v1/invite.js new file mode 100644 index 00000000..8d6c78ba --- /dev/null +++ b/front/src/utils/api/v1/invite.js @@ -0,0 +1,42 @@ +import lessonImage from '@sb-ui/resources/img/lesson.svg'; +// TODO: remove eslint-disables when it will be used after connecting with API +// eslint-disable-next-line no-unused-vars +import api from '@sb-ui/utils/api'; + +// eslint-disable-next-line no-unused-vars +const PATH = '/api/v1/invite'; + +export const getInvite = async ({ id }) => { + // TODO: use this instead of mocked data + // const { data } = await api.get(`${PATH}/${id}`); + + // Mocked data + const data = { + invite: id, + lesson: { + name: 'How to use Studybites', + description: + 'Open repair of infrarenal aortic aneurysm or dissection, plus repair of associated arterial trauma, following unsuccessful endovascular repair; tube prosthesis ', + author: { + firstName: 'John', + lastName: 'Galt', + }, + image: lessonImage, + }, + inviteUser: 'George Bakman', + keywords: [ + { + id: 1, + name: 'Tutorial', + }, + { + id: 2, + name: 'English', + }, + ], + email: 'my@mail.com', + isRegistered: false, + }; + + return data; +}; diff --git a/front/src/utils/paths.js b/front/src/utils/paths.js index 848e2f1f..30890bf7 100644 --- a/front/src/utils/paths.js +++ b/front/src/utils/paths.js @@ -7,6 +7,7 @@ export const VERIFY_EMAIL = '/verify-email/:id'; export const EMAIL_SENT = '/email-sent'; export const FORGOT_PASSWORD = '/forgot-password'; +export const INVITE = '/invite/:id'; export const PROFILE = '/profile'; export const USER_HOME = '/user'; export const USER_LESSONS = `${USER_HOME}/lessons`; diff --git a/front/src/utils/queries.js b/front/src/utils/queries.js index 8020b094..4b56668a 100644 --- a/front/src/utils/queries.js +++ b/front/src/utils/queries.js @@ -15,6 +15,8 @@ export const USER_ENROLLED_SHORT_LESSONS_BASE_KEY = 'user/enrolled_short_lessons'; export const LESSON_BASE_QUERY = 'user/lesson'; +export const INVITE_LESSON_QUERY = 'invite/lesson'; + // TEACHER export const TEACHER_LESSONS_BASE_KEY = 'teacher/lessons'; export const TEACHER_COURSES_BASE_KEY = 'teacher/courses';