Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions front/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ i18n.use(initReactI18next).init({
'change_password',
'forgot_password',
'verify_email',
'invite',
],
defaultNS: 'common',
keySeparator: '.',
Expand Down
94 changes: 94 additions & 0 deletions front/src/pages/Invite/Invite.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Page>
<S.Container>
<S.HeaderBlock>
{isLoading ? (
<Skeleton avatar paragraph={{ rows: 0 }} />
) : (
<>
<div>
<S.Avatar />
</div>
<S.HeaderTitle>
{t('header', { inviteUser, email })}
</S.HeaderTitle>
</>
)}
</S.HeaderBlock>
<S.BodyBlock>
<SM.ImageBlock>
{isLoading ? (
<Skeleton avatar paragraph={{ rows: 4 }} />
) : (
<>
<SM.Image src={image} alt="Lesson" />
<SM.AuthorContainer>
<SM.AuthorAvatar>{firstNameLetter}</SM.AuthorAvatar>
<SM.AuthorName>{fullName}</SM.AuthorName>
</SM.AuthorContainer>
</>
)}
</SM.ImageBlock>
<Row>
<SM.Title>{name}</SM.Title>
</Row>
<Row>
<SM.Description>{description}</SM.Description>
{keywords && (
<SM.KeywordsCol>
<LessonKeywords keywords={keywords} />
</SM.KeywordsCol>
)}
</Row>
{isAuth ? (
<S.JoinButton>{t('buttons.join')}</S.JoinButton>
) : (
<SignUp email={email} isRegistered={isRegistered} />
)}
</S.BodyBlock>
</S.Container>
</S.Page>
);
};

export default Invite;
66 changes: 66 additions & 0 deletions front/src/pages/Invite/Invite.styled.jsx
Original file line number Diff line number Diff line change
@@ -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: <UserOutlined />,
})``;

export const BodyBlock = styled.div`
background-color: white;
padding: 2rem;
`;

export const EmailInput = styled(Input)`
margin-bottom: 1.5rem;
`;
97 changes: 97 additions & 0 deletions front/src/pages/Invite/SignUp/SignUp.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Form
form={form}
onFieldsChange={handleFieldsChange}
onFinish={handleSubmit}
>
{error && (
<Form.Item>
<Alert
onClose={() => setError(null)}
message={t(error)}
type="error"
showIcon
closable
/>
</Form.Item>
)}

<S.EmailInput
value={email}
disabled
placeholder={t('email.placeholder')}
/>

{!isRegistered && (
<>
<Form.Item name="firstName" rules={formRules.firstName}>
<Input placeholder={t('first_name.placeholder')} />
</Form.Item>

<Form.Item name="lastName" rules={formRules.lastName}>
<Input placeholder={t('last_name.placeholder')} />
</Form.Item>
</>
)}

<Form.Item name="password" rules={formRules.password}>
<div>
<Input.Password placeholder={t('password.placeholder')} />
{!isRegistered && <PasswordStrengthIndicator value={password} />}
</div>
</Form.Item>

<S.Button loading={loading} disabled={isFormErrors}>
{t(buttonKey)}
</S.Button>
</S.Form>
);
};

SignUp.propTypes = {
email: T.string,
isRegistered: T.bool,
};

export default SignUp;
3 changes: 3 additions & 0 deletions front/src/pages/Invite/SignUp/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SignUp from './SignUp';

export default SignUp;
3 changes: 3 additions & 0 deletions front/src/pages/Invite/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Invite from './Invite';

export default Invite;
60 changes: 60 additions & 0 deletions front/src/pages/Invite/useForm.js
Original file line number Diff line number Diff line change
@@ -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 };
};
2 changes: 2 additions & 0 deletions front/src/resources/lang/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,5 +25,6 @@ export default {
editorjs,
email,
forgot_password,
invite,
verify_email,
};
8 changes: 8 additions & 0 deletions front/src/resources/lang/en/invite.js
Original file line number Diff line number Diff line change
@@ -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',
};
2 changes: 2 additions & 0 deletions front/src/resources/lang/ru/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,5 +25,6 @@ export default {
editorjs,
email,
forgot_password,
invite,
verify_email,
};
8 changes: 8 additions & 0 deletions front/src/resources/lang/ru/invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
buttons: {
sign_up: 'Создать аккаунт & Записаться',
sign_in: 'Ввойти & Записаться',
join: 'Присоединиться',
},
header: '{{inviteUser}} пригласил вас ({{email}}) на урок',
};
1 change: 1 addition & 0 deletions front/src/routes/PrivateRoutes/PrivateRoutes.utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const getPrivateRoutes = ({ isMobile }) => [
permissions: [Roles.SUPER_ADMIN],
exact: true,
},

{
component: Profile,
path: paths.PROFILE,
Expand Down
Loading