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';