Skip to content

Commit

Permalink
added create and save snippets feature for guest users
Browse files Browse the repository at this point in the history
  • Loading branch information
Svetlna committed Jan 29, 2024
1 parent a4749d6 commit 6a8d0e9
Show file tree
Hide file tree
Showing 14 changed files with 1,511 additions and 700 deletions.
4 changes: 2 additions & 2 deletions backend/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class UpdateUserDto {
@IsString()
@Matches(/^[\w\S]*$/)
@Validate(CheckUsername, {
message: 'Уже существует!',
message: 'usernameIsUsed',
})
username?: string;

Expand All @@ -39,7 +39,7 @@ export class UpdateUserDto {
@IsString()
@IsEmail()
@Validate(CheckEmail, {
message: 'Уже существует!',
message: 'emailIsUsed',
})
email?: string;

Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"proxy": "http://localhost:5001",
"dependencies": {
"@faker-js/faker": "^8.4.0",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5",
"@sentry/react": "^7.47.0",
Expand Down
211 changes: 211 additions & 0 deletions frontend/src/components/Forms/GuestSignUpForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import axios from 'axios';
import { useFormik } from 'formik';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { object } from 'yup';

import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import routes from '../../routes';
import { email, password, username } from '../../utils/validationSchemas';

import GithubSignInButton from './GithubSignInButton.jsx';
import PasswordVisibilityButton from './PasswordVisibilityButton.jsx';
import FormAlert from './FormAlert.jsx';
import { actions as userActions } from '../../slices/userSlice';
import { actions as modalActions } from '../../slices/modalSlice.js';

function GuestSignupForm() {
const { t } = useTranslation();
const emailRef = useRef();
const usernameRef = useRef();
const dispatch = useDispatch();
const navigate = useNavigate();

const userInfo = useSelector((state) => state.user.userInfo);

const initialFormState = { state: 'initial', message: '' };
const [formState, setFormState] = useState(initialFormState);

const [isPasswordVisible, setPasswordVisibility] = useState(false);
const handlePasswordVisibility = () => {
setPasswordVisibility(!isPasswordVisible);
};

useEffect(() => {
usernameRef.current.focus();
}, []);

const validationSchema = object().shape({
username: username(),
email: email(),
password: password(),
});

const formik = useFormik({
initialValues: {
username: '',
email: '',
password: '',
},
validationSchema,
validateOnBlur: false,
onSubmit: async (values, actions) => {
setFormState(initialFormState);
const preparedValues = validationSchema.cast(values);
console.log(preparedValues);
try {
actions.setSubmitting(true);

const response = await axios.put(routes.updateUserPath(userInfo.id), {
id: userInfo.id,
username: preparedValues.username,
email: preparedValues.email,
currPassword: JSON.parse(localStorage.getItem('guestUserData'))
.guestId,
password: values.password,
});
dispatch(userActions.setUserInfo(response.data));
localStorage.removeItem('guestUserData');
dispatch(modalActions.closeModal());
navigate(routes.profilePagePath(preparedValues.username));
actions.setSubmitting(false);
} catch (err) {
if (!err.isAxiosError) {
formik.resetForm();
setFormState({
state: 'failed',
message: 'errors.unknown',
});
throw err;
}
if (
err.response?.status === 400 &&
Array.isArray(err.response?.data?.errs?.message)
) {
err.response.data.errs.message.forEach((e) => {
switch (e) {
case 'usernameIsUsed':
actions.setFieldError(
'username',
'errors.validation.usernameIsUsed',
);
usernameRef.current.select();
break;
case 'emailIsUsed':
actions.setFieldError('email', 'errors.validation.emailIsUsed');
emailRef.current.select();
break;
default:
setFormState({
state: 'failed',
message: 'errors.network',
});
throw err;
}
});
} else {
formik.resetForm();
setFormState({
state: 'failed',
message: 'errors.network',
});
throw err;
}
}
},
});

return (
<div className="d-flex flex-column gap-4">
<FormAlert
onClose={() => setFormState(initialFormState)}
state={formState.state}
>
{t(formState.message)}
</FormAlert>
<Form
className="d-flex flex-column gap-1 gap-sm-3"
noValidate
onSubmit={formik.handleSubmit}
>
<div className="d-flex flex-column">
<Form.Group className="form-group" controlId="username">
<Form.Label>{t('profileSettings.usernameLabel')}</Form.Label>
<Form.Control
ref={usernameRef}
autoComplete="username"
isInvalid={!!formik.touched.username && !!formik.errors.username}
name="username"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
required
type="text"
value={formik.values.username}
/>
<Form.Control.Feedback type="invalid">
{t(formik.errors.username)}
</Form.Control.Feedback>
</Form.Group>

<Form.Group className="form-group" controlId="email">
<Form.Label>{t('profileSettings.emailLabel')}</Form.Label>
<Form.Control
ref={emailRef}
autoComplete="email"
isInvalid={!!formik.touched.email && !!formik.errors.email}
name="email"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
required
type="email"
value={formik.values.email}
/>
<Form.Control.Feedback type="invalid">
{t(formik.errors.email)}
</Form.Control.Feedback>
</Form.Group>

<Form.Group className="form-group" controlId="password">
<Form.Label>{t('profileSettings.passwordLabel')}</Form.Label>
<div className="input-group-inline-button">
<Form.Control
autoComplete="new-password"
isInvalid={
!!formik.touched.password && !!formik.errors.password
}
name="password"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
required
type={isPasswordVisible ? 'text' : 'password'}
value={formik.values.password}
/>
<PasswordVisibilityButton
enabled={isPasswordVisible}
onClick={handlePasswordVisibility}
/>
<Form.Control.Feedback type="invalid">
{t(formik.errors.password)}
</Form.Control.Feedback>
</div>
</Form.Group>
</div>
<Button
className="mb-2"
disabled={formik.isSubmitting}
type="submit"
variant="primary"
>
{t('signUp.registerButton')}
</Button>
</Form>
<GithubSignInButton />
</div>
);
}

export default GuestSignupForm;
48 changes: 46 additions & 2 deletions frontend/src/components/Modals/NewSnippet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useFormik } from 'formik';
import { object } from 'yup';
import { faker } from '@faker-js/faker';

import { Typeahead } from 'react-bootstrap-typeahead';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';
import Image from 'react-bootstrap/Image';
import Form from 'react-bootstrap/Form';

import { useSnippets } from '../../hooks';
import axios from 'axios';
import routes from '../../routes';

import { useAuth, useSnippets } from '../../hooks';
import { snippetName } from '../../utils/validationSchemas';
import { actions as modalActions } from '../../slices/modalSlice.js';
import JavaScriptIcon from '../../assets/images/icons/javascript.svg';
Expand All @@ -28,8 +32,16 @@ const extensions = new Map()
.set('python', 'py')
.set('php', 'php');

const generateGuestUserData = () => {
const username = `guest_${faker.string.alphanumeric(5)}`;
const email = `${username}@hexlet.com`;
const password = `guest_${faker.internet.password()}`;
return { username, email, password };
};

function NewSnippet({ handleClose, isOpen }) {
const { t } = useTranslation();
const auth = useAuth();
const dispatch = useDispatch();
const snippetApi = useSnippets();
const navigate = useNavigate();
Expand Down Expand Up @@ -58,6 +70,36 @@ function NewSnippet({ handleClose, isOpen }) {
validationSchema,
onSubmit: async (values, { setSubmitting }) => {
setSubmitting(true);
const guestData = !auth.isLoggedIn && generateGuestUserData();
const targetUsername = auth.isLoggedIn ? username : guestData.username;
if (!auth.isLoggedIn) {
try {
await axios.post(routes.usersPath(), guestData);
auth.signIn();
localStorage.setItem(
'guestUserData',
JSON.stringify({
guestId: guestData.password,
}),
);
} catch (err) {
if (!err.isAxiosError) {
console.log('errors.unknown');
throw err;
}
if (
err.response?.status === 400 &&
Array.isArray(err.response?.data?.errs?.message)
) {
// случай, когда случайно сгенерировался username или email, который уже есть в базе
console.log(t('errors.network'));
throw err;
} else {
console.log(t('errors.network'));
throw err;
}
}
}
const [language] = selectedLng;
const code = t(`codeTemplates.${language}`);
// TODO: Тут не должно быть проверок, нужно создать абстракцию сервиса, который будет работать с любыми языками
Expand All @@ -66,7 +108,9 @@ function NewSnippet({ handleClose, isOpen }) {
const snipName = `${values.name}.${extensions.get(language)}`;
const id = await snippetApi.saveSnippet(code, snipName, language);
const { slug } = await snippetApi.getSnippetData(id);
const url = new URL(snippetApi.genViewSnippetLink(username, slug));
const url = new URL(
snippetApi.genViewSnippetLink(targetUsername, slug),
);
console.log(id, slug, url.pathname);
formik.values.name = '';
navigate(url.pathname);
Expand Down
35 changes: 23 additions & 12 deletions frontend/src/components/Modals/SignUp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { useDispatch } from 'react-redux';

import { actions } from '../../slices';
import SignupForm from '../Forms/SignUpForm';
import GuestSignupForm from '../Forms/GuestSignUpForm';
import { useAuth } from '../../hooks';

function SignUpModal({ handleClose, isOpen }) {
const dispatch = useDispatch();
const { t } = useTranslation();
const auth = useAuth();

const guestUser = localStorage.getItem('guestUserData');

return (
<Modal centered onHide={handleClose} show={isOpen}>
Expand All @@ -18,18 +23,24 @@ function SignUpModal({ handleClose, isOpen }) {
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SignupForm onSuccess={handleClose} />
<div className="d-flex justify-content-center align-items-baseline mt-5">
<span className="text-body-secondary">
{t('signUp.footer.signInHeader')}
</span>{' '}
<Button
onClick={() => dispatch(actions.openModal({ type: 'signingIn' }))}
variant="link"
>
{t('profileActions.signIn')}
</Button>
</div>
{auth.isLoggedIn && guestUser ? (
<GuestSignupForm />
) : (
<SignupForm onSuccess={handleClose} />
)}
{!guestUser && (
<div className="d-flex justify-content-center align-items-baseline mt-5">
<span className="text-body-secondary">
{t('signUp.footer.signInHeader')}
</span>{' '}
<Button
onClick={() => dispatch(actions.openModal({ type: 'signingIn' }))}
variant="link"
>
{t('profileActions.signIn')}
</Button>
</div>
)}
</Modal.Body>
</Modal>
);
Expand Down
Loading

0 comments on commit 6a8d0e9

Please sign in to comment.