├── .prettierignore
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── manifest.json
│ ├── favicon.svg
│ └── index.html
├── src
│ ├── blocks
│ │ ├── popup
│ │ │ ├── __container
│ │ │ │ ├── popup__container.css
│ │ │ │ └── _type
│ │ │ │ │ └── popup__container_type_form.css
│ │ │ ├── _type
│ │ │ │ └── popup_type_img.css
│ │ │ ├── __figure-wrapper
│ │ │ │ └── popup__figure-wrapper.css
│ │ │ ├── _opened
│ │ │ │ └── popup_opened.css
│ │ │ ├── __status-icon
│ │ │ │ ├── popup__status-icon.css
│ │ │ │ └── _type
│ │ │ │ │ ├── popup__status-icon_type_fail.css
│ │ │ │ │ └── popup__status-icon_type_success.css
│ │ │ ├── __img
│ │ │ │ └── popup__img.css
│ │ │ ├── __img-caption
│ │ │ │ └── popup__img-caption.css
│ │ │ ├── __status-text
│ │ │ │ └── popup__status-text.css
│ │ │ ├── __title
│ │ │ │ └── popup__title.css
│ │ │ ├── popup.css
│ │ │ ├── __status-wrapper
│ │ │ │ └── popup__status-wrapper.css
│ │ │ └── __btn-close
│ │ │ │ └── popup__btn-close.css
│ │ ├── preloader
│ │ │ ├── _active
│ │ │ │ └── preloader_active.css
│ │ │ └── preloader.css
│ │ ├── form
│ │ │ ├── __input-error
│ │ │ │ ├── _active
│ │ │ │ │ └── form__input-error_active.css
│ │ │ │ └── form__input-error.css
│ │ │ ├── __input
│ │ │ │ ├── _type
│ │ │ │ │ └── form__input_type_error.css
│ │ │ │ ├── _place
│ │ │ │ │ └── form__input_place_authorization.css
│ │ │ │ └── form__input.css
│ │ │ ├── _type
│ │ │ │ └── form_type_card-delete-confirmation.css
│ │ │ ├── _place
│ │ │ │ └── form_place_authorization.css
│ │ │ ├── form.css
│ │ │ └── __btn-submit
│ │ │ │ ├── _place
│ │ │ │ └── form__btn-submit_place_authorization.css
│ │ │ │ └── form__btn-submit.css
│ │ ├── hamburger
│ │ │ ├── _active
│ │ │ │ └── hamburger_active.css
│ │ │ ├── __user-email
│ │ │ │ └── hamburger__user-email.css
│ │ │ ├── hamburger.css
│ │ │ └── __btn-sign-out
│ │ │ │ └── hamburger__btn-sign-out.css
│ │ ├── card
│ │ │ ├── __like-wrapper
│ │ │ │ └── card__like-wrapper.css
│ │ │ ├── __img
│ │ │ │ └── card__img.css
│ │ │ ├── __btn-like
│ │ │ │ ├── _active
│ │ │ │ │ └── card__btn-like_active.css
│ │ │ │ └── card__btn-like.css
│ │ │ ├── __like-counter
│ │ │ │ └── card__like-counter.css
│ │ │ ├── __caption
│ │ │ │ └── card__caption.css
│ │ │ ├── __title
│ │ │ │ └── card__title.css
│ │ │ ├── card.css
│ │ │ └── __btn-del
│ │ │ │ └── card__btn-del.css
│ │ ├── footer
│ │ │ ├── footer.css
│ │ │ └── __copyright
│ │ │ │ └── footer__copyright.css
│ │ ├── header
│ │ │ ├── __logo
│ │ │ │ └── header__logo.css
│ │ │ ├── __btn-hamburger
│ │ │ │ ├── _type
│ │ │ │ │ └── header__btn-hamburger_type_close.css
│ │ │ │ └── header__btn-hamburger.css
│ │ │ ├── __nav
│ │ │ │ └── header__nav.css
│ │ │ ├── __user-email
│ │ │ │ └── header__user-email.css
│ │ │ ├── __link
│ │ │ │ └── header__link.css
│ │ │ ├── header.css
│ │ │ └── __btn-sign-out
│ │ │ │ └── header__btn-sign-out.css
│ │ ├── profile
│ │ │ ├── __avatar
│ │ │ │ └── profile__avatar.css
│ │ │ ├── __wrapper
│ │ │ │ └── profile__wrapper.css
│ │ │ ├── __user-edit
│ │ │ │ └── profile__user-edit.css
│ │ │ ├── __user-wrapper
│ │ │ │ └── profile__user-wrapper.css
│ │ │ ├── profile.css
│ │ │ ├── __user-about
│ │ │ │ └── profile__user-about.css
│ │ │ ├── __btn-add
│ │ │ │ └── profile__btn-add.css
│ │ │ ├── __user-name
│ │ │ │ └── profile__user-name.css
│ │ │ ├── __btn-edit
│ │ │ │ └── profile__btn-edit.css
│ │ │ └── __btn-avatar-edit
│ │ │ │ └── profile__btn-avatar-edit.css
│ │ ├── authorization
│ │ │ ├── __link
│ │ │ │ └── authorization__link.css
│ │ │ ├── __text
│ │ │ │ └── authorization__text.css
│ │ │ ├── __wrapper
│ │ │ │ └── authorization__wrapper.css
│ │ │ ├── authorization.css
│ │ │ └── __title
│ │ │ │ └── authorization__title.css
│ │ ├── page
│ │ │ ├── __content
│ │ │ │ └── page__content.css
│ │ │ └── page.css
│ │ ├── cards
│ │ │ ├── cards.css
│ │ │ └── __wrapper
│ │ │ │ └── cards__wrapper.css
│ │ ├── not-found
│ │ │ ├── not-found.css
│ │ │ ├── __title
│ │ │ │ └── not-found__title.css
│ │ │ ├── __description
│ │ │ │ └── not-found__description.css
│ │ │ └── __link
│ │ │ │ └── not-found__link.css
│ │ ├── logo
│ │ │ └── logo.css
│ │ └── content
│ │ │ └── content.css
│ ├── fonts
│ │ ├── Inter-Black.woff
│ │ ├── Inter-Black.woff2
│ │ ├── Inter-Medium.woff
│ │ ├── Inter-Medium.woff2
│ │ ├── Inter-Regular.woff
│ │ └── Inter-Regular.woff2
│ ├── contexts
│ │ └── CurrentUserContext.js
│ ├── images
│ │ ├── hamburger.svg
│ │ ├── profile-add.svg
│ │ ├── avatar-edit.svg
│ │ ├── profile-edit.svg
│ │ ├── close-icon.svg
│ │ ├── card-like-active.svg
│ │ ├── success.svg
│ │ ├── fail.svg
│ │ ├── card-like.svg
│ │ ├── trash.svg
│ │ ├── preloader.svg
│ │ ├── logo-light.svg
│ │ └── logo-dark.svg
│ ├── components
│ │ ├── Preloader.js
│ │ ├── Footer.js
│ │ ├── ProtectedRoute.js
│ │ ├── HamburgerMenu.js
│ │ ├── Header.js
│ │ ├── NotFound.js
│ │ ├── ImagePopup.js
│ │ ├── AppLayout.js
│ │ ├── PopupWithForm.js
│ │ ├── DeleteCardPopup.js
│ │ ├── Form.js
│ │ ├── AuthScreen.js
│ │ ├── InfoTooltip.js
│ │ ├── Popup.js
│ │ ├── NavBar.js
│ │ ├── Card.js
│ │ ├── EditAvatarPopup.js
│ │ ├── Main.js
│ │ ├── Login.js
│ │ ├── Register.js
│ │ ├── AddPlacePopup.js
│ │ ├── EditProfilePopup.js
│ │ └── App.js
│ ├── setupTests.js
│ ├── reportWebVitals.js
│ ├── index.js
│ ├── vendor
│ │ ├── font.css
│ │ └── normalize.css
│ ├── utils
│ │ ├── useValidation.js
│ │ └── api.js
│ └── index.css
├── .gitignore
└── package.json
├── screenshot
└── mesto_1.png
├── backend
├── .eslintrc
├── middlewares
│ ├── limiter.js
│ ├── errors.js
│ ├── cors.js
│ ├── logger.js
│ └── auth.js
├── controllers
│ ├── notFound.js
│ ├── cards.js
│ └── users.js
├── .gitignore
├── routes
│ ├── notFound.js
│ ├── signin.js
│ ├── index.js
│ ├── signup.js
│ ├── users.js
│ └── cards.js
├── utils
│ ├── config.js
│ └── constants.js
├── errors
│ ├── incorrectDataError.js
│ ├── conflictError.js
│ ├── forbiddenError.js
│ ├── notFoundError.js
│ └── authorizationError.js
├── .editorconfig
├── models
│ ├── card.js
│ └── user.js
├── package.json
└── app.js
├── .github
└── workflows
│ └── tests.yml
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.*
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__container/popup__container.css:
--------------------------------------------------------------------------------
1 | .popup__container {
2 | position: relative;
3 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/preloader/_active/preloader_active.css:
--------------------------------------------------------------------------------
1 | .preloader_active {
2 | visibility: visible;
3 | }
--------------------------------------------------------------------------------
/screenshot/mesto_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/screenshot/mesto_1.png
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/_type/popup_type_img.css:
--------------------------------------------------------------------------------
1 | .popup_type_img {
2 | background-color: rgba(0, 0, 0, .9);
3 | }
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__figure-wrapper/popup__figure-wrapper.css:
--------------------------------------------------------------------------------
1 | .popup__figure-wrapper {
2 | margin: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__input-error/_active/form__input-error_active.css:
--------------------------------------------------------------------------------
1 | .form__input-error_active {
2 | opacity: 1;
3 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/_opened/popup_opened.css:
--------------------------------------------------------------------------------
1 | .popup_opened {
2 | visibility: visible;
3 | opacity: 1;
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/src/blocks/hamburger/_active/hamburger_active.css:
--------------------------------------------------------------------------------
1 | .hamburger_active {
2 | position: relative;
3 | top: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__status-icon/popup__status-icon.css:
--------------------------------------------------------------------------------
1 | .popup__status-icon {
2 | width: 120px;
3 | height: 120px;
4 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__input/_type/form__input_type_error.css:
--------------------------------------------------------------------------------
1 | .form__input_type_error {
2 | border-bottom-color: #FF0000;
3 | }
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Black.woff
--------------------------------------------------------------------------------
/frontend/src/blocks/form/_type/form_type_card-delete-confirmation.css:
--------------------------------------------------------------------------------
1 | .form_type_card-delete-confirmation {
2 | padding-top: 14px;
3 | }
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Black.woff2
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Medium.woff
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Medium.woff2
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/frontend/src/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bjorn86/react-mesto-api-full-gha/HEAD/frontend/src/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/backend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "rules": {
4 | "no-underscore-dangle": ["error", { "allow": ["_id"] }]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__like-wrapper/card__like-wrapper.css:
--------------------------------------------------------------------------------
1 | .card__like-wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/src/blocks/footer/footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | flex-shrink: 0;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__logo/header__logo.css:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: 896px) {
2 | .header__logo {
3 | margin-left: 27px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/contexts/CurrentUserContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | // CURRENT USER CONTEXT
4 | export const CurrentUserContext = createContext();
5 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__img/card__img.css:
--------------------------------------------------------------------------------
1 | .card__img {
2 | width: 282px;
3 | height: 282px;
4 | object-fit: cover;
5 | cursor: pointer;
6 | color: #fff;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__avatar/profile__avatar.css:
--------------------------------------------------------------------------------
1 | .profile__avatar {
2 | width: 100%;
3 | height: 100%;
4 | object-fit: cover;
5 | border-radius: 50%;
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/images/hamburger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__img/popup__img.css:
--------------------------------------------------------------------------------
1 | .popup__img {
2 | max-width: 75vw;
3 | max-height: 75vh;
4 | object-fit: cover;
5 | vertical-align: bottom;
6 | color: #fff;
7 | }
8 |
--------------------------------------------------------------------------------
/backend/middlewares/limiter.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const rateLimit = require('express-rate-limit');
3 |
4 | module.exports = rateLimit({
5 | windowMs: 15 * 60 * 1000,
6 | max: 100,
7 | });
8 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__status-icon/_type/popup__status-icon_type_fail.css:
--------------------------------------------------------------------------------
1 | .popup__status-icon_type_fail {
2 | background: url('../../../../images/fail.svg') center center/cover no-repeat;
3 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__btn-like/_active/card__btn-like_active.css:
--------------------------------------------------------------------------------
1 | .card__btn-like_active {
2 | background: url('../../../../images/card-like-active.svg') center center/contain no-repeat;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__status-icon/_type/popup__status-icon_type_success.css:
--------------------------------------------------------------------------------
1 | .popup__status-icon_type_success {
2 | background: url('../../../../images/success.svg') center center/cover no-repeat;
3 | }
--------------------------------------------------------------------------------
/frontend/src/images/profile-add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__like-counter/card__like-counter.css:
--------------------------------------------------------------------------------
1 | .card__like-counter {
2 | margin: 0;
3 | font-weight: 400;
4 | font-size: 13px;
5 | line-height: 1.23;
6 | text-align: center;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__img-caption/popup__img-caption.css:
--------------------------------------------------------------------------------
1 | .popup__img-caption {
2 | margin-top: 10px;
3 | font-weight: 400;
4 | font-size: 12px;
5 | line-height: 1.25;
6 | color: #fff;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/blocks/authorization/__link/authorization__link.css:
--------------------------------------------------------------------------------
1 | .authorization__link {
2 | text-decoration: none;
3 | transition: opacity .6s;
4 | }
5 |
6 | .authorization__link:hover {
7 | opacity: 0.6;
8 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/hamburger/__user-email/hamburger__user-email.css:
--------------------------------------------------------------------------------
1 | .hamburger__user-email {
2 | margin: 0;
3 | font-weight: 500;
4 | font-size: 18px;
5 | line-height: 1.22;
6 | color: #ffffff;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/images/avatar-edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/images/profile-edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__btn-hamburger/_type/header__btn-hamburger_type_close.css:
--------------------------------------------------------------------------------
1 | .header__btn-hamburger_type_close {
2 | background: url('../../../../images/close-icon.svg') center center/20px no-repeat, transparent;
3 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/page/__content/page__content.css:
--------------------------------------------------------------------------------
1 | .page__content {
2 | min-width: 320px;
3 | max-width: 880px;
4 | min-height: 100vh;
5 | margin: 0 auto;
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/blocks/authorization/__text/authorization__text.css:
--------------------------------------------------------------------------------
1 | .authorization__text {
2 | margin: 15px 0 0;
3 | font-weight: 400;
4 | font-size: 14px;
5 | line-height: 1.21;
6 | text-align: center;
7 | color: #fff;
8 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__caption/card__caption.css:
--------------------------------------------------------------------------------
1 | .card__caption {
2 | height: 80px;
3 | display: flex;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 0 20px 0 21px;
7 | background-color: #fff;
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/blocks/cards/cards.css:
--------------------------------------------------------------------------------
1 | .cards {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | }
5 |
6 | @media screen and (max-width: 896px) {
7 | .cards {
8 | padding: 0 19px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/_place/form_place_authorization.css:
--------------------------------------------------------------------------------
1 | .form_place_authorization {
2 | padding: 50px 0 0;
3 | }
4 |
5 | @media screen and (max-width: 544px) {
6 | .form_place_authorization {
7 | padding-top: 40px;
8 | }
9 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__nav/header__nav.css:
--------------------------------------------------------------------------------
1 | .header__nav {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | @media screen and (max-width: 896px) {
7 | .header__nav {
8 | padding-right: 30px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/components/Preloader.js:
--------------------------------------------------------------------------------
1 | // PRELOADER COMPONENT
2 | function Preloader({ isActive }) {
3 | return (
4 |
5 | );
6 | }
7 |
8 | export default Preloader;
9 |
--------------------------------------------------------------------------------
/backend/controllers/notFound.js:
--------------------------------------------------------------------------------
1 | // IMPORT ERRORS
2 | const NotFoundError = require('../errors/notFoundError');
3 |
4 | // NOT FOUNDED ROUTE
5 | module.exports.notFound = (req, res, next) => {
6 | next(new NotFoundError('Указан несуществующий URL'));
7 | };
8 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__title/card__title.css:
--------------------------------------------------------------------------------
1 | .card__title {
2 | margin: 0;
3 | font-weight: 900;
4 | font-size: 24px;
5 | line-height: 1.21;
6 | text-overflow: ellipsis;
7 | white-space: nowrap;
8 | overflow: hidden;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/images/close-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/not-found/not-found.css:
--------------------------------------------------------------------------------
1 | .not-found {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | padding: 60px 19px;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | flex-grow: 1;
9 | }
10 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Dependency directory
7 | node_modules
8 |
9 | # Optional npm cache directory
10 | .npm
11 |
12 | # Optional REPL history
13 | .DS_Store
14 | .idea
15 | .vscode
16 |
17 | # Env variables
18 | *.env
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__input-error/form__input-error.css:
--------------------------------------------------------------------------------
1 | .form__input-error {
2 | display: block;
3 | min-height: 25px;
4 | margin-top: 5px;
5 | opacity: 0;
6 | font-weight: 400;
7 | font-size: 12px;
8 | line-height: 1.04;
9 | color: #FF0000;
10 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/card/card.css:
--------------------------------------------------------------------------------
1 | .card {
2 | font-family: "Inter", "Arial", sans-serif;
3 | width: 282px;
4 | height: 362px;
5 | position: relative;
6 | display: flex;
7 | flex-direction: column;
8 | border-radius: 10px;
9 | overflow: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/blocks/page/page.css:
--------------------------------------------------------------------------------
1 | .page {
2 | min-height: 100vh;
3 | max-width: 100vw;
4 | background-color: #000;
5 | padding-bottom: 60px;
6 | }
7 |
8 | @media screen and (max-width: 544px) {
9 | .page {
10 | padding-bottom: 36px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__input/_place/form__input_place_authorization.css:
--------------------------------------------------------------------------------
1 | .form__input_place_authorization {
2 | border-bottom: 2px solid #CCC;
3 | background-color: transparent;
4 | color: #fff;
5 | }
6 |
7 | .form__input_place_authorization::placeholder {
8 | color: #CCC;
9 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__input/form__input.css:
--------------------------------------------------------------------------------
1 | .form__input {
2 | width: 100%;
3 | height: 27px;
4 | padding: 0;
5 | border: none;
6 | border-bottom: 1px solid rgba(0, 0, 0, .2);
7 | outline: none;
8 | font-weight: 400;
9 | font-size: 14px;
10 | line-height: 1.21;
11 | }
--------------------------------------------------------------------------------
/backend/routes/notFound.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const router = require('express').Router();
3 |
4 | // IMPORT CONTROLLERS
5 | const { notFound } = require('../controllers/notFound');
6 |
7 | // NOT FOUNDED ROUTE
8 | router.all('/*', notFound);
9 |
10 | // MODULE EXPORT
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | // FOOTER COMPONENT
2 | function Footer() {
3 | return (
4 |
7 | );
8 | }
9 |
10 | export default Footer;
11 |
--------------------------------------------------------------------------------
/frontend/src/blocks/authorization/__wrapper/authorization__wrapper.css:
--------------------------------------------------------------------------------
1 | .authorization__wrapper {
2 | max-width: 358px;
3 | margin: 0 auto;
4 | }
5 |
6 | @media screen and (max-width: 544px) {
7 | .authorization__wrapper {
8 | padding-left: 30px;
9 | padding-right: 30px;
10 | }
11 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/logo/logo.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | width: 142px;
3 | height: 33px;
4 | background: url('../../images/logo-light.svg') center center/contain no-repeat;
5 | }
6 |
7 | @media screen and (max-width: 544px) {
8 | .logo {
9 | width: 104px;
10 | height: 25px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__wrapper/profile__wrapper.css:
--------------------------------------------------------------------------------
1 | .profile__wrapper {
2 | display: flex;
3 | gap: 30px;
4 | }
5 |
6 | @media screen and (max-width: 544px) {
7 | .profile__wrapper {
8 | gap: 26px;
9 | flex-direction: column;
10 | align-items: center;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/authorization/authorization.css:
--------------------------------------------------------------------------------
1 | .authorization {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | padding: 60px 0;
5 | flex-grow: 1;
6 | }
7 |
8 | @media screen and (max-width: 544px) {
9 | .authorization {
10 | padding-top: 40px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__user-edit/profile__user-edit.css:
--------------------------------------------------------------------------------
1 | .profile__user-edit {
2 | max-width: 100%;
3 | display: flex;
4 | align-items: baseline;
5 | gap: 18px;
6 | }
7 |
8 | @media screen and (max-width: 544px) {
9 | .profile__user-edit {
10 | position: relative;
11 | gap: 10px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/backend/utils/config.js:
--------------------------------------------------------------------------------
1 | // DEFAULT VALUES
2 | const MODE_PRODUCTION = 'production';
3 | const DEV_KEY = 'dev-secret-key';
4 | const DEFAULT_PORT = 3000;
5 | const DEFAULT_DATABASE = 'mongodb://localhost:27017/mestodb';
6 |
7 | module.exports = {
8 | MODE_PRODUCTION,
9 | DEV_KEY,
10 | DEFAULT_PORT,
11 | DEFAULT_DATABASE,
12 | };
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/not-found/__title/not-found__title.css:
--------------------------------------------------------------------------------
1 | .not-found__title {
2 | margin: 0;
3 | font-weight: 900;
4 | font-size: 32px;
5 | line-height: 1.21;
6 | text-align: center;
7 | color: #fff;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .not-found__title {
12 | font-size: 24px;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/blocks/preloader/preloader.css:
--------------------------------------------------------------------------------
1 | .preloader {
2 | font-family: "Inter", "Arial", sans-serif;
3 | width: 100%;
4 | height: 100%;
5 | position: fixed;
6 | visibility: hidden;
7 | left: 0;
8 | top: 0;
9 | background: url("../../images/preloader.svg") center center/auto no-repeat, rgb(0, 0, 0);
10 | }
11 |
--------------------------------------------------------------------------------
/backend/errors/incorrectDataError.js:
--------------------------------------------------------------------------------
1 | const { BAD_REQUEST_ERROR_CODE } = require('../utils/constants');
2 |
3 | // AUTHORIZATION ERROR
4 | class IncorrectDataError extends Error {
5 | constructor(message) {
6 | super(message);
7 | this.statusCode = BAD_REQUEST_ERROR_CODE;
8 | }
9 | }
10 |
11 | module.exports = IncorrectDataError;
12 |
--------------------------------------------------------------------------------
/backend/errors/conflictError.js:
--------------------------------------------------------------------------------
1 | // IMPORT VARIABLES
2 | const { CONFLICT_ERROR_CODE } = require('../utils/constants');
3 |
4 | // AUTHORIZATION ERROR
5 | class ConflictError extends Error {
6 | constructor(message) {
7 | super(message);
8 | this.statusCode = CONFLICT_ERROR_CODE;
9 | }
10 | }
11 |
12 | module.exports = ConflictError;
13 |
--------------------------------------------------------------------------------
/backend/errors/forbiddenError.js:
--------------------------------------------------------------------------------
1 | // IMPORT VARIABLES
2 | const { FORBIDDEN_ERROR_CODE } = require('../utils/constants');
3 |
4 | // AUTHORIZATION ERROR
5 | class ForbiddenError extends Error {
6 | constructor(message) {
7 | super(message);
8 | this.statusCode = FORBIDDEN_ERROR_CODE;
9 | }
10 | }
11 |
12 | module.exports = ForbiddenError;
13 |
--------------------------------------------------------------------------------
/backend/errors/notFoundError.js:
--------------------------------------------------------------------------------
1 | // IMPORT VARIABLES
2 | const { NOT_FOUND_ERROR_CODE } = require('../utils/constants');
3 |
4 | // AUTHORIZATION ERROR
5 | class NotFoundError extends Error {
6 | constructor(message) {
7 | super(message);
8 | this.statusCode = NOT_FOUND_ERROR_CODE;
9 | }
10 | }
11 |
12 | module.exports = NotFoundError;
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/form.css:
--------------------------------------------------------------------------------
1 | .form {
2 | box-sizing: border-box;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | padding: 46px 36px 37px;
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .form {
12 | padding: 73px 22px 25px;
13 | }
14 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__user-email/header__user-email.css:
--------------------------------------------------------------------------------
1 | .header__user-email {
2 | margin: 0;
3 | padding-right: 24px;
4 | font-weight: 500;
5 | font-size: 18px;
6 | line-height: 1.22;
7 | color: #ffffff;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .header__user-email {
12 | display: none;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/blocks/not-found/__description/not-found__description.css:
--------------------------------------------------------------------------------
1 | .not-found__description {
2 | margin: 30px 0 0;
3 | font-weight: 400;
4 | font-size: 16px;
5 | line-height: 1.21;
6 | text-align: center;
7 | color: #fff;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .not-found__description {
12 | font-size: 14px;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__status-text/popup__status-text.css:
--------------------------------------------------------------------------------
1 | .popup__status-text {
2 | margin: 32px 0 0;
3 | font-weight: 900;
4 | font-size: 24px;
5 | line-height: 1.21;
6 | text-align: center;
7 | }
8 |
9 | @media screen and (max-width: 544px) {
10 | .popup__status-text {
11 | margin-top: 40px;
12 | font-size: 20px;
13 | }
14 | }
--------------------------------------------------------------------------------
/backend/errors/authorizationError.js:
--------------------------------------------------------------------------------
1 | // IMPORT VARIABLES
2 | const { UNAUTHORIZED_ERROR_CODE } = require('../utils/constants');
3 |
4 | // AUTHORIZATION ERROR
5 | class AuthorizationError extends Error {
6 | constructor(message) {
7 | super(message);
8 | this.statusCode = UNAUTHORIZED_ERROR_CODE;
9 | }
10 | }
11 |
12 | module.exports = AuthorizationError;
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__btn-like/card__btn-like.css:
--------------------------------------------------------------------------------
1 | .card__btn-like {
2 | width: 22px;
3 | height: 19px;
4 | padding: 0;
5 | cursor: pointer;
6 | border: none;
7 | background: url('../../../images/card-like.svg') center center/contain no-repeat;
8 | transition: opacity .6s;
9 | }
10 |
11 | .card__btn-like:hover {
12 | opacity: .5;
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/blocks/content/content.css:
--------------------------------------------------------------------------------
1 | .content {
2 | max-width: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | gap: 50px;
6 | padding: 40px 0 66px;
7 | flex-grow: 1;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .content {
12 | padding-top: 42px;
13 | padding-bottom: 48px;
14 | gap: 36px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/blocks/hamburger/hamburger.css:
--------------------------------------------------------------------------------
1 | .hamburger {
2 | font-family: "Inter", "Arial", sans-serif;
3 | position: absolute;
4 | top: -143px;
5 | max-width: 100%;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | gap: 18px;
10 | padding: 40px 0 40px;
11 | border-bottom: 1px solid rgba(84, 84, 84, 0.7);
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/components/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 |
3 | //
4 | const ProtectedRouteElement = ({ element: Component, ...props }) => {
5 | return props.loggedIn ? (
6 |
7 | ) : (
8 |
9 | );
10 | };
11 |
12 | export default ProtectedRouteElement;
13 |
--------------------------------------------------------------------------------
/frontend/src/images/card-like-active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/authorization/__title/authorization__title.css:
--------------------------------------------------------------------------------
1 | .authorization__title {
2 | margin: 0;
3 | font-weight: 900;
4 | font-size: 24px;
5 | line-height: 1.21;
6 | color: #fff;
7 | text-align: center;
8 | }
9 |
10 | @media screen and (max-width: 544px) {
11 | .authorization__title {
12 | font-size: 20px;
13 | line-height: 1.2;
14 | }
15 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__user-wrapper/profile__user-wrapper.css:
--------------------------------------------------------------------------------
1 | .profile__user-wrapper {
2 | max-width: 336px;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | gap: 8px;
7 | }
8 |
9 | @media screen and (max-width: 544px) {
10 | .profile__user-wrapper {
11 | max-width: 220px;
12 | align-items: center;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Mesto",
3 | "name": "Mesto",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__container/_type/popup__container_type_form.css:
--------------------------------------------------------------------------------
1 | .popup__container_type_form {
2 | width: 100%;
3 | max-width: 430px;
4 | background-color: #fff;
5 | box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.15);
6 | border-radius: 10px;
7 | }
8 |
9 | @media screen and (max-width: 544px) {
10 | .popup__container_type_form {
11 | margin: 0 19px;
12 | }
13 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/profile.css:
--------------------------------------------------------------------------------
1 | .profile {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | }
8 |
9 | @media screen and (max-width: 896px) {
10 | .profile {
11 | padding: 0 19px;
12 | flex-direction: column;
13 | gap: 32px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__link/header__link.css:
--------------------------------------------------------------------------------
1 | .header__link {
2 | font-weight: 400;
3 | font-size: 18px;
4 | line-height: 1.21;
5 | color: #fff;
6 | text-decoration: none;
7 | transition: opacity .6s;
8 | }
9 |
10 | .header__link:hover {
11 | opacity: 0.6;
12 | }
13 |
14 | @media screen and (max-width: 544px) {
15 | .header__link {
16 | font-size: 14px;
17 | }
18 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__title/popup__title.css:
--------------------------------------------------------------------------------
1 | .popup__title {
2 | margin: 0;
3 | padding: 34px 36px 0;
4 | font-weight: 900;
5 | font-size: 24px;
6 | line-height: 1.21;
7 | }
8 |
9 | @media screen and (max-width: 544px) {
10 | .popup__title {
11 | padding-top: 25px;
12 | padding-right: 22px;
13 | padding-left: 22px;
14 | font-size: 18px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/blocks/cards/__wrapper/cards__wrapper.css:
--------------------------------------------------------------------------------
1 | .cards__wrapper {
2 | display: grid;
3 | grid-template-columns: repeat(auto-fit, minmax(282px, 1fr));
4 | justify-items: center;
5 | gap: 20px 17px;
6 | list-style: none;
7 | margin: 0 auto;
8 | padding: 0;
9 | }
10 |
11 | @media screen and (max-width: 896px) {
12 | .cards__wrapper {
13 | max-width: 581px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/popup.css:
--------------------------------------------------------------------------------
1 | .popup {
2 | font-family: "Inter", "Arial", sans-serif;
3 | width: 100%;
4 | height: 100%;
5 | position: fixed;
6 | left: 0;
7 | top: 0;
8 | visibility: hidden;
9 | opacity: 0;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | background-color: rgba(0, 0, 0, .5);
14 | transition: visibility .6s, opacity .6s;
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/images/success.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/card/__btn-del/card__btn-del.css:
--------------------------------------------------------------------------------
1 | .card__btn-del {
2 | width: 18px;
3 | height: 19px;
4 | position: absolute;
5 | top: 20px;
6 | right: 20px;
7 | padding: 0;
8 | cursor: pointer;
9 | border: none;
10 | background: url('../../../images/trash.svg') center center/contain no-repeat;
11 | transition: opacity .6s;
12 | }
13 |
14 | .card__btn-del:hover {
15 | opacity: .6;
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/blocks/not-found/__link/not-found__link.css:
--------------------------------------------------------------------------------
1 | .not-found__link {
2 | margin: 15px 0 0;
3 | font-weight: 400;
4 | font-size: 16px;
5 | line-height: 1.21;
6 | text-decoration: none;
7 | color: #fff;
8 | transition: opacity 0.6s;
9 | }
10 |
11 | .not-found__link:hover {
12 | opacity: 0.6;
13 | }
14 |
15 | @media screen and (max-width: 544px) {
16 | .not-found__link {
17 | font-size: 14px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__status-wrapper/popup__status-wrapper.css:
--------------------------------------------------------------------------------
1 | .popup__status-wrapper {
2 | box-sizing: border-box;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | padding: 60px 36px;
9 | }
10 |
11 | @media screen and (max-width: 544px) {
12 | .popup__status-wrapper {
13 | padding: 50px 18px;
14 | }
15 | }
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | font-family: "Inter", "Arial", sans-serif;
3 | max-width: 100%;
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | padding: 45px 0 41px;
8 | border-bottom: 1px solid rgba(84, 84, 84, 0.7);
9 | }
10 |
11 | @media screen and (max-width: 544px) {
12 | .header {
13 | padding-top: 28px;
14 | padding-bottom: 30px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/blocks/footer/__copyright/footer__copyright.css:
--------------------------------------------------------------------------------
1 | .footer__copyright {
2 | margin: 0;
3 | font-weight: 400;
4 | font-size: 18px;
5 | line-height: 1.21;
6 | color: #545454;
7 | }
8 |
9 | @media screen and (max-width: 896px) {
10 | .footer__copyright {
11 | padding-left: 19px;
12 | }
13 | }
14 |
15 | @media screen and (max-width: 544px) {
16 | .footer__copyright {
17 | font-size: 14px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/frontend/src/blocks/hamburger/__btn-sign-out/hamburger__btn-sign-out.css:
--------------------------------------------------------------------------------
1 | .hamburger__btn-sign-out {
2 | width: 56px;
3 | height: 22px;
4 | background-color: transparent;
5 | padding: 0;
6 | border: none;
7 | cursor: pointer;
8 | font-weight: 400;
9 | font-size: 18px;
10 | line-height: 1.22;
11 | color: #a9a9a9;
12 | transition: opacity 0.6s;
13 | }
14 |
15 | .hamburger__btn-sign-out:hover {
16 | opacity: 0.6;
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/images/fail.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/routes/signin.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const router = require('express').Router();
3 | const { celebrate, Joi } = require('celebrate');
4 |
5 | // IMPORT CONTROLLERS
6 | const { login } = require('../controllers/users');
7 |
8 | // LOGIN ROUTE
9 | router.post('/', celebrate({
10 | body: Joi.object().keys({
11 | email: Joi.string().required().email(),
12 | password: Joi.string().required(),
13 | }),
14 | }), login);
15 |
16 | // MODULE EXPORT
17 | module.exports = router;
18 |
--------------------------------------------------------------------------------
/backend/middlewares/errors.js:
--------------------------------------------------------------------------------
1 | // IMPORT VARIABLES
2 | const { DEFAULT_ERROR_CODE } = require('../utils/constants');
3 |
4 | // ERRORS MIDDLEWARE
5 | module.exports = (err, req, res, next) => {
6 | const statusCode = err.statusCode || DEFAULT_ERROR_CODE;
7 | const errorMessage = statusCode === DEFAULT_ERROR_CODE ? `Произошла неизвестная ошибка ${err.name}: ${err.message}` : err.message;
8 | res.status(statusCode).send({
9 | message: errorMessage,
10 | });
11 | return next();
12 | };
13 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__user-about/profile__user-about.css:
--------------------------------------------------------------------------------
1 | .profile__user-about {
2 | max-width: 85%;
3 | margin: 0;
4 | font-weight: 400;
5 | font-size: 18px;
6 | line-height: 1.21;
7 | color: #fff;
8 | text-overflow: ellipsis;
9 | white-space: nowrap;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 544px) {
14 | .profile__user-about {
15 | max-width: 90%;
16 | font-size: 14px;
17 | text-align: center;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/components/HamburgerMenu.js:
--------------------------------------------------------------------------------
1 | // HAMBURGER MENU COMPONENT
2 | function HamburgerMenu({ email, isOpen, onLogOut }) {
3 | return (
4 |
5 |
{email || ""}
6 |
13 |
14 | );
15 | }
16 |
17 | export default HamburgerMenu;
18 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.js:
--------------------------------------------------------------------------------
1 | // IMPORT COMPONENTS
2 | import NavBar from "./NavBar";
3 |
4 | // HEADER COMPONENT
5 | function Header({ email, onHamburgerClick, isOpen, onLogOut }) {
6 | return (
7 |
16 | );
17 | }
18 |
19 | export default Header;
20 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__btn-hamburger/header__btn-hamburger.css:
--------------------------------------------------------------------------------
1 | .header__btn-hamburger {
2 | display: none;
3 | width: 24px;
4 | height: 22px;
5 | background: url("../../../images/hamburger.svg") center center/contain no-repeat, transparent;
6 | padding: 0;
7 | border: none;
8 | cursor: pointer;
9 | transition: opacity 0.6s;
10 | }
11 |
12 | .header__btn-hamburger:hover {
13 | opacity: 0.6;
14 | }
15 |
16 | @media screen and (max-width: 544px) {
17 | .header__btn-hamburger {
18 | display: block;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/blocks/header/__btn-sign-out/header__btn-sign-out.css:
--------------------------------------------------------------------------------
1 | .header__btn-sign-out {
2 | width: 56px;
3 | height: 22px;
4 | background-color: transparent;
5 | padding: 0;
6 | border: none;
7 | cursor: pointer;
8 | font-weight: 400;
9 | font-size: 18px;
10 | line-height: 1.22;
11 | color: #a9a9a9;
12 | transition: opacity 0.6s;
13 | }
14 |
15 | .header__btn-sign-out:hover {
16 | opacity: 0.6;
17 | }
18 |
19 | @media screen and (max-width: 544px) {
20 | .header__btn-sign-out {
21 | display: none;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // NOT FOUND COMPONENT
4 | function NotFound() {
5 | return (
6 |
7 | 404 - Страница не найдена
8 |
9 | Извините, страница которую вы ищите не найдена.
10 |
11 |
12 | Вернуться на главную
13 |
14 |
15 | );
16 | }
17 |
18 | export default NotFound;
19 |
--------------------------------------------------------------------------------
/frontend/src/images/card-like.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__btn-add/profile__btn-add.css:
--------------------------------------------------------------------------------
1 | .profile__btn-add {
2 | min-width: 150px;
3 | min-height: 50px;
4 | padding: 0;
5 | cursor: pointer;
6 | border: 2px solid #fff;
7 | border-radius: 2px;
8 | background: url('../../../images/profile-add.svg') center center/22px no-repeat;
9 | transition: opacity .6s;
10 | }
11 |
12 | .profile__btn-add:hover {
13 | opacity: .6;
14 | }
15 |
16 | @media screen and (max-width: 544px) {
17 | .profile__btn-add {
18 | min-width: 282px;
19 | background-size: 16px;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/ImagePopup.js:
--------------------------------------------------------------------------------
1 | import Popup from "./Popup";
2 |
3 | // IMAGE POPUP COMPONENT
4 | function ImagePopup({ card, onClose }) {
5 | return (
6 |
11 |
12 |
13 | {card?.name}
14 |
15 |
16 | );
17 | }
18 |
19 | export default ImagePopup;
20 |
--------------------------------------------------------------------------------
/frontend/src/images/trash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__user-name/profile__user-name.css:
--------------------------------------------------------------------------------
1 | .profile__user-name {
2 | width: 100%;
3 | margin: 0;
4 | font-weight: 500;
5 | font-size: 42px;
6 | line-height: 1.14;
7 | color: #fff;
8 | text-overflow: ellipsis;
9 | white-space: nowrap;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 896px) {
14 | .profile__user-name {
15 | font-size: 34px;
16 | }
17 | }
18 |
19 | @media screen and (max-width: 544px) {
20 | .profile__user-name {
21 | font-size: 27px;
22 | line-height: 1.21;
23 | text-align: center;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/blocks/popup/__btn-close/popup__btn-close.css:
--------------------------------------------------------------------------------
1 | .popup__btn-close {
2 | min-width: 32px;
3 | min-height: 32px;
4 | top: -40px;
5 | right: -40px;
6 | position: absolute;
7 | padding: 0;
8 | border: none;
9 | cursor: pointer;
10 | background: url('../../../images/close-icon.svg') center center/contain no-repeat;
11 | transition: opacity .6s;
12 | }
13 |
14 | .popup__btn-close:hover {
15 | opacity: .6;
16 | }
17 |
18 | @media screen and (max-width: 544px) {
19 | .popup__btn-close {
20 | min-width: 20px;
21 | min-height: 20px;
22 | top: -36px;
23 | right: 0;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__btn-edit/profile__btn-edit.css:
--------------------------------------------------------------------------------
1 | .profile__btn-edit {
2 | display: inline-flex;
3 | min-width: 24px;
4 | min-height: 24px;
5 | padding: 0;
6 | cursor: pointer;
7 | border: 1px solid #fff;
8 | background: url('../../../images/profile-edit.svg') center center/10px no-repeat;
9 | transition: opacity .6s;
10 | }
11 |
12 | .profile__btn-edit:hover {
13 | opacity: .6;
14 | }
15 |
16 | @media screen and (max-width: 544px) {
17 | .profile__btn-edit {
18 | position: absolute;
19 | min-width: 18px;
20 | min-height: 18px;
21 | top: 8px;
22 | right: -28px;
23 | background-size: 7.5px;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/routes/index.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const rootRouter = require('express').Router();
3 |
4 | // IMPORT ROUTES
5 | const signin = require('./signin');
6 | const signup = require('./signup');
7 | const users = require('./users');
8 | const cards = require('./cards');
9 | const notFound = require('./notFound');
10 |
11 | // IMPORT MIDDLEWARES
12 | const auth = require('../middlewares/auth');
13 |
14 | // ROUTES METHODS
15 | rootRouter.use('/signin', signin);
16 | rootRouter.use('/signup', signup);
17 | rootRouter.use('/users', auth, users);
18 | rootRouter.use('/cards', auth, cards);
19 | rootRouter.use('*', auth, notFound);
20 |
21 | // EXPORT ROUTES
22 | module.exports = rootRouter;
23 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__btn-submit/_place/form__btn-submit_place_authorization.css:
--------------------------------------------------------------------------------
1 | .form__btn-submit_place_authorization {
2 | margin-top: 186px;
3 | background-color: #fff;
4 | color: #000;
5 | }
6 |
7 | .form__btn-submit_place_authorization:hover {
8 | opacity: 0.85;
9 | }
10 |
11 | .form__btn-submit_place_authorization:disabled {
12 | background-color: #CCC;
13 | border: 1px solid rgba(255, 255, 255, .2);
14 | color: rgba(0, 0, 0, .4);
15 | pointer-events: none;
16 | }
17 |
18 | @media screen and (max-width: 544px) {
19 | .form__btn-submit_place_authorization {
20 | margin-top: 143px;
21 | font-size: 16px;
22 | line-height: 1.19;
23 | }
24 | }
--------------------------------------------------------------------------------
/frontend/src/blocks/profile/__btn-avatar-edit/profile__btn-avatar-edit.css:
--------------------------------------------------------------------------------
1 | .profile__btn-avatar-edit {
2 | width: 120px;
3 | height: 120px;
4 | position: relative;
5 | padding: 0;
6 | border: none;
7 | border-radius: 50%;
8 | cursor: pointer;
9 | background-color: transparent;
10 | }
11 |
12 | .profile__btn-avatar-edit::before {
13 | content: '';
14 | width: 100%;
15 | height: 100%;
16 | position: absolute;
17 | top: 0;
18 | right: 0;
19 | background: url("../../../images/avatar-edit.svg") center center/auto no-repeat, rgb(0, 0, 0);
20 | opacity: 0;
21 | transition: opacity .6s;
22 | }
23 |
24 | .profile__btn-avatar-edit:hover::before {
25 | opacity: 0.8;
26 | }
27 |
--------------------------------------------------------------------------------
/backend/routes/signup.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const router = require('express').Router();
3 | const { celebrate, Joi } = require('celebrate');
4 |
5 | // IMPORT CONTROLLERS
6 | const { createUser } = require('../controllers/users');
7 |
8 | // IMPORT VARIABLES
9 | const { LINK_REGEXP } = require('../utils/constants');
10 |
11 | // LOGIN ROUTE
12 | router.post('/', celebrate({
13 | body: Joi.object().keys({
14 | name: Joi.string().min(2).max(30),
15 | about: Joi.string().min(2).max(30),
16 | avatar: Joi.string().regex(LINK_REGEXP),
17 | email: Joi.string().required().email(),
18 | password: Joi.string().required(),
19 | }),
20 | }), createUser);
21 |
22 | // MODULE EXPORT
23 | module.exports = router;
24 |
--------------------------------------------------------------------------------
/backend/middlewares/cors.js:
--------------------------------------------------------------------------------
1 | // CORS VARIABLES
2 | const { ALLOWED_CORS, DEFAULT_ALLOWED_METHODS } = require('../utils/constants');
3 |
4 | // CORS MIDDLEWARE
5 | module.exports = (req, res, next) => {
6 | const { origin } = req.headers;
7 | const { method } = req;
8 | const requestHeaders = req.headers['access-control-request-headers'];
9 | if (ALLOWED_CORS.includes(origin)) {
10 | res.header('Access-Control-Allow-Origin', origin);
11 | res.header('Access-Control-Allow-Credentials', true);
12 | }
13 | if (method === 'OPTIONS') {
14 | res.header('Access-Control-Allow-Methods', DEFAULT_ALLOWED_METHODS);
15 | res.header('Access-Control-Allow-Headers', requestHeaders);
16 | return res.end();
17 | }
18 | return next();
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 | import "./index.css";
5 | import App from "./components/App";
6 | import reportWebVitals from "./reportWebVitals";
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/frontend/src/vendor/font.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Inter;
3 | font-display: swap;
4 | src: url("../fonts/Inter-Regular.woff2") format("woff2"), url("../fonts/Inter-Regular.woff") format("woff");
5 | font-weight: 400;
6 | font-style: normal;
7 | }
8 |
9 | @font-face {
10 | font-family: Inter;
11 | font-display: swap;
12 | src: url("../fonts/Inter-Medium.woff2") format("woff2"), url("../fonts/Inter-Medium.woff") format("woff");
13 | font-weight: 500;
14 | font-style: normal;
15 | }
16 |
17 | @font-face {
18 | font-family: Inter;
19 | font-display: swap;
20 | src: url("../fonts/Inter-Black.woff2") format("woff2"), url("../fonts/Inter-Black.woff") format("woff");
21 | font-weight: 900;
22 | font-style: normal;
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/components/AppLayout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 |
3 | // IMPORT COMPONENTS
4 | import Footer from "./Footer";
5 | import HamburgerMenu from "./HamburgerMenu";
6 | import Header from "./Header";
7 |
8 | // APP LAYOUT COMPONENT
9 | function AppLayout({ email, isOpen, onHamburgerClick, onLogOut }) {
10 | return (
11 | <>
12 |
17 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
29 | export default AppLayout;
30 |
--------------------------------------------------------------------------------
/frontend/src/blocks/form/__btn-submit/form__btn-submit.css:
--------------------------------------------------------------------------------
1 | .form__btn-submit {
2 | min-height: 50px;
3 | padding: 0;
4 | margin-top: 18px;
5 | border: none;
6 | background-color: #000;
7 | cursor: pointer;
8 | border-radius: 2px;
9 | font-weight: 400;
10 | font-size: 18px;
11 | line-height: 1.21;
12 | color: #fff;
13 | transition: opacity .6s;
14 | }
15 |
16 | .form__btn-submit:hover {
17 | opacity: .8;
18 | }
19 |
20 | .form__btn-submit:disabled {
21 | background-color: #fff;
22 | border: 1px solid rgba(0, 0, 0, .2);
23 | color: rgba(0, 0, 0, .2);
24 | pointer-events: none;
25 | }
26 |
27 | @media screen and (max-width: 544px) {
28 | .form__btn-submit {
29 | min-height: 46px;
30 | margin-top: 15px;
31 | font-size: 14px;
32 | }
33 | }
--------------------------------------------------------------------------------
/frontend/src/components/PopupWithForm.js:
--------------------------------------------------------------------------------
1 | // IMPORT COMPONENTS
2 | import Form from "./Form";
3 | import Popup from "./Popup";
4 |
5 | // POPUP WITH FORM COMPONENT
6 | function PopupWithForm({
7 | name,
8 | title,
9 | buttonText,
10 | isOpen,
11 | onClose,
12 | onSubmit,
13 | isFormValid,
14 | ...props
15 | }) {
16 | return (
17 |
22 | {title}
23 |
31 |
32 | );
33 | }
34 |
35 | export default PopupWithForm;
36 |
--------------------------------------------------------------------------------
/backend/middlewares/logger.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const winston = require('winston');
3 | const expressWinston = require('express-winston');
4 |
5 | // REQUEST LOGGER
6 | const requestLogger = expressWinston.logger({
7 | transports: [
8 | new winston.transports.File({
9 | filename: 'logs/request.log',
10 | maxsize: '10000000',
11 | maxFiles: '10',
12 | }),
13 | ],
14 | format: winston.format.json(),
15 | });
16 |
17 | // ERROR LOGGER
18 | const errorLogger = expressWinston.errorLogger({
19 | transports: [
20 | new winston.transports.File({
21 | filename: 'logs/error.log',
22 | maxsize: '10000000',
23 | maxFiles: '10',
24 | }),
25 | ],
26 | format: winston.format.json(),
27 | });
28 |
29 | module.exports = {
30 | requestLogger,
31 | errorLogger,
32 | };
33 |
--------------------------------------------------------------------------------
/backend/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const jwt = require('jsonwebtoken');
3 |
4 | // IMPORT ERRORS
5 | const AuthorizationError = require('../errors/authorizationError');
6 |
7 | // CONFIG VARIABLES
8 | const { NODE_ENV, SECRET_KEY } = process.env;
9 | const { MODE_PRODUCTION, DEV_KEY } = require('../utils/config');
10 |
11 | // AUTHORIZATION MIDDLEWARE
12 | module.exports = (req, res, next) => {
13 | const token = req.cookies.jwt;
14 | if (!token) {
15 | return next(new AuthorizationError('Необходима авторизация'));
16 | }
17 | let payload;
18 | try {
19 | payload = jwt.verify(token, NODE_ENV === MODE_PRODUCTION ? SECRET_KEY : DEV_KEY);
20 | } catch (err) {
21 | return next(new AuthorizationError('Необходима авторизация'));
22 | }
23 | req.user = payload;
24 | return next();
25 | };
26 |
--------------------------------------------------------------------------------
/frontend/src/components/DeleteCardPopup.js:
--------------------------------------------------------------------------------
1 | // IMPORT COMPONENTS
2 | import PopupWithForm from "./PopupWithForm";
3 |
4 | // DELETE CARD POPUP COMPONENT
5 | function DeleteCardPopup({
6 | isOpen,
7 | onClose,
8 | onDeleteCard,
9 | onLoading,
10 | card,
11 | onOverlayClick,
12 | }) {
13 | // HANDLE SUBMIT
14 | function handleSubmit(e) {
15 | e.preventDefault();
16 | onDeleteCard(card);
17 | }
18 | return (
19 |
29 | );
30 | }
31 |
32 | export default DeleteCardPopup;
33 |
--------------------------------------------------------------------------------
/frontend/src/images/preloader.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/components/Form.js:
--------------------------------------------------------------------------------
1 | //
2 | function Form({ name, buttonText, onSubmit, isFormValid, ...props }) {
3 | return (
4 |
30 | );
31 | }
32 |
33 | export default Form;
34 |
--------------------------------------------------------------------------------
/backend/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | [*]
8 | # Indentation style
9 | # Possible values - tab, space
10 | indent_style = space
11 |
12 | # Indentation size in single-spaced characters
13 | # Possible values - an integer, tab
14 | indent_size = 2
15 |
16 | # Line ending file format
17 | # Possible values - lf, crlf, cr
18 | end_of_line = lf
19 |
20 | # File character encoding
21 | # Possible values - latin1, utf-8, utf-16be, utf-16le
22 | charset = utf-8
23 |
24 | # Denotes whether to trim whitespace at the end of lines
25 | # Possible values - true, false
26 | trim_trailing_whitespace = true
27 |
28 | [*.md]
29 | trim_trailing_whitespace = false
30 |
31 | # Denotes whether file should end with a newline
32 | # Possible values - true, false
33 | insert_final_newline = true
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
18 |
19 |
20 |
21 | Место
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/backend/utils/constants.js:
--------------------------------------------------------------------------------
1 | // STATUS CODES
2 | const CREATE_CODE = 201;
3 | const BAD_REQUEST_ERROR_CODE = 400;
4 | const UNAUTHORIZED_ERROR_CODE = 401;
5 | const FORBIDDEN_ERROR_CODE = 403;
6 | const NOT_FOUND_ERROR_CODE = 404;
7 | const CONFLICT_ERROR_CODE = 409;
8 | const DEFAULT_ERROR_CODE = 500;
9 |
10 | // REGEXP
11 | const LINK_REGEXP = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9-._~:/?#[\]@!$&'()*+,;=]*)?$/im;
12 |
13 | // ALLOWED CORS DOMAINS
14 | const ALLOWED_CORS = [
15 | 'http://mesto.ld-webdev.ru',
16 | 'https://mesto.ld-webdev.ru',
17 | 'http://77.232.131.208',
18 | 'https://77.232.131.208',
19 | 'http://localhost:3000',
20 | 'http://localhost:3001',
21 | ];
22 |
23 | // ALLOWED METHODS
24 | const DEFAULT_ALLOWED_METHODS = 'GET,HEAD,PUT,PATCH,POST,DELETE';
25 |
26 | module.exports = {
27 | CREATE_CODE,
28 | BAD_REQUEST_ERROR_CODE,
29 | UNAUTHORIZED_ERROR_CODE,
30 | FORBIDDEN_ERROR_CODE,
31 | NOT_FOUND_ERROR_CODE,
32 | CONFLICT_ERROR_CODE,
33 | DEFAULT_ERROR_CODE,
34 | LINK_REGEXP,
35 | ALLOWED_CORS,
36 | DEFAULT_ALLOWED_METHODS,
37 | };
38 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mesto-auth",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "gh-pages": "^5.0.0",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-router-dom": "^6.9.0",
13 | "react-scripts": "5.0.1",
14 | "web-vitals": "^2.1.4"
15 | },
16 | "scripts": {
17 | "predeploy": "npm run build",
18 | "deploy": "gh-pages -d build",
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/models/card.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const mongoose = require('mongoose');
3 |
4 | // IMPORT VARIABLES
5 | const { LINK_REGEXP } = require('../utils/constants');
6 |
7 | // CARD SCHEMA
8 | const cardSchema = new mongoose.Schema({
9 | name: {
10 | type: String,
11 | required: [true, 'Поле "name" должно быть заполнено'],
12 | minlength: [2, 'Минимальная длина поля "name" 2 символа'],
13 | maxlength: [30, 'Максимальная длина поля "name" 30 символов'],
14 | },
15 | link: {
16 | type: String,
17 | required: [true, 'Поле "link" должно быть заполнено'],
18 | validate: {
19 | validator: (v) => LINK_REGEXP.test(v),
20 | message: 'Неправильный формат ссылки',
21 | },
22 | },
23 | owner: {
24 | type: mongoose.Schema.Types.ObjectId,
25 | ref: 'user',
26 | required: [true, 'Поле "owner" должно быть заполнено'],
27 | },
28 | likes: [{
29 | type: mongoose.Schema.Types.ObjectId,
30 | ref: 'user',
31 | default: [],
32 | }],
33 | createdAt: {
34 | type: Date,
35 | default: Date.now,
36 | },
37 | }, { versionKey: false });
38 |
39 | // MODULE EXPORT
40 | module.exports = mongoose.model('card', cardSchema);
41 |
--------------------------------------------------------------------------------
/frontend/src/components/AuthScreen.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // IMPORT COMPONENTS
4 | import Form from "./Form";
5 |
6 | // AUTHORIZATION SCREEN COMPONENT
7 | function AuthScreen({
8 | name,
9 | title,
10 | buttonText,
11 | onSubmit,
12 | isFormValid,
13 | ...props
14 | }) {
15 | return (
16 |
17 |
18 |
{title}
19 |
27 | {name === "registr" && (
28 |
29 | Уже зарегистрированы?{" "}
30 |
34 | Войти
35 |
36 |
37 | )}
38 |
39 |
40 | );
41 | }
42 |
43 | export default AuthScreen;
44 |
--------------------------------------------------------------------------------
/frontend/src/utils/useValidation.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | // USE VALIDATION CUSTOM HOOK
4 | function useValidation() {
5 | // STATE VARIABLES WITH HOOKS
6 | const [values, setValues] = useState({});
7 | const [errors, setErrors] = useState({});
8 | const [isFormValid, setFormValid] = useState(false);
9 | // HANDLE INPUTS CHANGE
10 | function onChange(e) {
11 | const { name, value } = e.target;
12 | const error = e.target.validationMessage;
13 | const formValid = e.target.closest("form").checkValidity();
14 | setValues((values) => ({ ...values, [name]: value }));
15 | setErrors((errors) => ({ ...errors, [name]: error }));
16 | setFormValid(formValid);
17 | }
18 | // HANDLE RESET VALIDATION ERRORS
19 | const resetValidation = useCallback(
20 | (isFormValid = false, values = {}, errors = {}) => {
21 | setFormValid(isFormValid);
22 | setValues(values);
23 | setErrors(errors);
24 | },
25 | [setFormValid, setValues, setErrors]
26 | );
27 | return {
28 | values,
29 | errors,
30 | isFormValid,
31 | onChange,
32 | resetValidation,
33 | };
34 | }
35 |
36 | export default useValidation;
37 |
--------------------------------------------------------------------------------
/frontend/src/components/InfoTooltip.js:
--------------------------------------------------------------------------------
1 | // IMPORT COMPONENTS
2 | import Popup from "./Popup";
3 |
4 | // INFORMATION TOOLTIP COMPONENT
5 | function InfoTooltip({ isOpen, onClose, status }) {
6 | // HANDLE CLASS TOGGLE
7 | function handleClassToggle(status) {
8 | if (status === "success") {
9 | return "popup__status-icon_type_success";
10 | } else if (status === "fail") {
11 | return "popup__status-icon_type_fail";
12 | } else {
13 | return "";
14 | }
15 | }
16 | // HANDLE TEXT TOGGLE
17 | function handleTextToggle(status) {
18 | if (status === "success") {
19 | return "Вы успешно зарегистрировались!";
20 | } else if (status === "fail") {
21 | return "Что-то пошло не так! Попробуйте ещё раз.";
22 | } else {
23 | return "";
24 | }
25 | }
26 | return (
27 |
32 |
33 |
36 |
{handleTextToggle(status)}
37 |
38 |
39 | );
40 | }
41 |
42 | export default InfoTooltip;
43 |
--------------------------------------------------------------------------------
/backend/routes/users.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const router = require('express').Router();
3 | const { celebrate, Joi } = require('celebrate');
4 |
5 | // IMPORT CONTROLLERS
6 | const {
7 | getAllUsers,
8 | getUser,
9 | getUserInfo,
10 | updateUserInfo,
11 | updateUserAvatar,
12 | logout,
13 | } = require('../controllers/users');
14 |
15 | // IMPORT VARIABLES
16 | const { LINK_REGEXP } = require('../utils/constants');
17 |
18 | // GET ALL USERS ROUTE
19 | router.get('/', getAllUsers);
20 |
21 | // GET USER INFO ROUTE
22 | router.get('/me', getUserInfo);
23 |
24 | // LOGOUT ROUTE
25 | router.delete('/me', logout);
26 |
27 | // GET USER ROUTE
28 | router.get('/:userId', celebrate({
29 | params: Joi.object().keys({
30 | userId: Joi.string().required().hex().length(24),
31 | }),
32 | }), getUser);
33 |
34 | // UPDATE USER INFO ROUTE
35 | router.patch('/me', celebrate({
36 | body: Joi.object().keys({
37 | name: Joi.string().required().min(2).max(30),
38 | about: Joi.string().required().min(2).max(30),
39 | }),
40 | }), updateUserInfo);
41 |
42 | // UPDATE USER AVATAR ROUTE
43 | router.patch('/me/avatar', celebrate({
44 | body: Joi.object().keys({
45 | avatar: Joi.string().required().regex(LINK_REGEXP),
46 | }),
47 | }), updateUserAvatar);
48 |
49 | // MODULE EXPORT
50 | module.exports = router;
51 |
--------------------------------------------------------------------------------
/frontend/src/components/Popup.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback } from "react";
2 |
3 | // POPUP COMPONENT
4 | function Popup({ isOpen, onClose, type, ...props }) {
5 | // HANDLE CLOSE POPUP BY ESC BUTTON
6 | useEffect(() => {
7 | function handleEscClose(evt) {
8 | if (evt.key === "Escape") {
9 | onClose();
10 | }
11 | }
12 | if (isOpen) {
13 | document.addEventListener("keydown", handleEscClose);
14 | return () => document.removeEventListener("keydown", handleEscClose);
15 | }
16 | }, [onClose, isOpen]);
17 | // HANDLE CLOSE BY CLICK ON OVERLAY
18 | const closeByClickOnOverlay = useCallback(
19 | (evt) => {
20 | if (evt.target === evt.currentTarget) {
21 | onClose();
22 | }
23 | },
24 | [onClose]
25 | );
26 | return (
27 |
31 |
32 | {props.children}
33 |
38 |
39 |
40 | );
41 | }
42 |
43 | export default Popup;
44 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mesto-api-full-gha",
3 | "version": "1.0.0",
4 | "description": "Mesto Project",
5 | "main": "app.js",
6 | "scripts": {
7 | "lint": "npx eslint ./",
8 | "start": "node app.js",
9 | "dev": "nodemon app.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/Bjorn86/react-mesto-api-full-gha.git"
14 | },
15 | "keywords": [
16 | "backend",
17 | "frontend"
18 | ],
19 | "author": "Danila Legkobytov",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/Bjorn86/react-mesto-api-full-gha/issues"
23 | },
24 | "homepage": "https://github.com/Bjorn86/react-mesto-api-full-gha#readme",
25 | "devDependencies": {
26 | "eslint": "^8.38.0",
27 | "eslint-config-airbnb-base": "^15.0.0",
28 | "eslint-plugin-import": "^2.27.5",
29 | "nodemon": "^2.0.22"
30 | },
31 | "dependencies": {
32 | "bcryptjs": "^2.4.3",
33 | "celebrate": "^15.0.1",
34 | "cookie-parser": "^1.4.6",
35 | "dotenv": "^16.0.3",
36 | "express": "^4.18.2",
37 | "express-rate-limit": "^6.7.0",
38 | "express-winston": "^4.2.0",
39 | "helmet": "^6.1.5",
40 | "jsonwebtoken": "^9.0.0",
41 | "mongoose": "^7.0.3",
42 | "validator": "^13.9.0",
43 | "winston": "^3.8.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/backend/routes/cards.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const router = require('express').Router();
3 | const { celebrate, Joi } = require('celebrate');
4 |
5 | // IMPORT CONTROLLERS
6 | const {
7 | getAllCards,
8 | createCard,
9 | deleteCard,
10 | likeCard,
11 | dislikeCard,
12 | } = require('../controllers/cards');
13 |
14 | // IMPORT VARIABLES
15 | const { LINK_REGEXP } = require('../utils/constants');
16 |
17 | // GET ALL CARDS ROUTE
18 | router.get('/', getAllCards);
19 |
20 | // CREATE CARD ROUTE
21 | router.post('/', celebrate({
22 | body: Joi.object().keys({
23 | name: Joi.string().required().min(2).max(30),
24 | link: Joi.string().required().regex(LINK_REGEXP),
25 | }),
26 | }), createCard);
27 |
28 | // DELETE CARD ROUTE
29 | router.delete('/:cardId', celebrate({
30 | params: Joi.object().keys({
31 | cardId: Joi.string().required().hex().length(24),
32 | }),
33 | }), deleteCard);
34 |
35 | // LIKE CARD ROUTE
36 | router.put('/:cardId/likes', celebrate({
37 | params: Joi.object().keys({
38 | cardId: Joi.string().required().hex().length(24),
39 | }),
40 | }), likeCard);
41 |
42 | // DISLIKE CARD ROUTE
43 | router.delete('/:cardId/likes', celebrate({
44 | params: Joi.object().keys({
45 | cardId: Joi.string().required().hex().length(24),
46 | }),
47 | }), dislikeCard);
48 |
49 | // MODULE EXPORT
50 | module.exports = router;
51 |
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | // IMPORT PACKAGES
4 | const express = require('express');
5 | const mongoose = require('mongoose');
6 | const cookieParser = require('cookie-parser');
7 | const helmet = require('helmet');
8 | const validationErrors = require('celebrate').errors;
9 |
10 | // IMPORT ROUTES
11 | const rootRouter = require('./routes/index');
12 |
13 | // IMPORT MIDDLEWARES
14 | const limiter = require('./middlewares/limiter');
15 | const errors = require('./middlewares/errors');
16 | const { requestLogger, errorLogger } = require('./middlewares/logger');
17 | const cors = require('./middlewares/cors');
18 |
19 | // CONFIG VARIABLES
20 | const { PORT, DATABASE } = process.env;
21 | const { DEFAULT_PORT, DEFAULT_DATABASE } = require('./utils/config');
22 |
23 | // APP VARIABLES
24 | const app = express();
25 |
26 | // DATABASE CONNECT
27 | mongoose.connect(DATABASE || DEFAULT_DATABASE, { authSource: 'admin' });
28 |
29 | // PARSERS METHODS
30 | app.use(express.json());
31 | app.use(cookieParser());
32 |
33 | // DEFENSE MIDDLEWARES
34 | app.use(helmet());
35 | app.use(limiter);
36 | app.use(cors);
37 |
38 | // REQUEST LOGGER
39 | app.use(requestLogger);
40 |
41 | // ROUTES METHOD
42 | app.use('/', rootRouter);
43 |
44 | // ERROR LOGGER
45 | app.use(errorLogger);
46 |
47 | // ERRORS HANDLER MIDDLEWARES
48 | app.use(validationErrors());
49 | app.use(errors);
50 |
51 | // SERVER LISTENER
52 | app.listen(PORT || DEFAULT_PORT);
53 |
--------------------------------------------------------------------------------
/frontend/src/components/NavBar.js:
--------------------------------------------------------------------------------
1 | import { Link, Route, Routes } from "react-router-dom";
2 |
3 | // NAVIGATION BAR COMPONENT
4 | function NavBar({ email, onHamburgerClick, isOpen, onLogOut }) {
5 | return (
6 |
7 |
8 |
12 | Войти
13 |
14 | }
15 | />
16 |
20 | Регистрация
21 |
22 | }
23 | />
24 |
28 | {email || ""}
29 |
36 |
43 | >
44 | }
45 | />
46 |
47 |
48 | );
49 | }
50 |
51 | export default NavBar;
52 |
--------------------------------------------------------------------------------
/frontend/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | // IMPORT CONTEXT
4 | import { CurrentUserContext } from "../contexts/CurrentUserContext";
5 |
6 | // CARD COMPONENT
7 | function Card({ card, onCardClick, onCardLike, onCardDelete }) {
8 | // CONTEXT VARIABLES
9 | const currentUser = useContext(CurrentUserContext);
10 | // OTHER VARIABLES
11 | const isOwn = card.owner === currentUser._id;
12 | const isLiked = card.likes.some((item) => item === currentUser._id);
13 | // HANDLE CARD IMAGE CLICK
14 | function handleClick() {
15 | onCardClick(card);
16 | }
17 | // HANDLE LIKE CLICK
18 | function handleLikeClick() {
19 | onCardLike(card);
20 | }
21 | // HANDLE DELETE CLICK
22 | function handleDeleteClick() {
23 | onCardDelete(card);
24 | }
25 | return (
26 |
27 | {isOwn && (
28 |
33 | )}
34 |
40 |
41 |
{card.name}
42 |
43 |
50 |
{card.likes.length}
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default Card;
58 |
--------------------------------------------------------------------------------
/frontend/src/components/EditAvatarPopup.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import useValidation from "../utils/useValidation";
3 |
4 | // IMPORT COMPONENTS
5 | import PopupWithForm from "./PopupWithForm";
6 |
7 | // EDIT AVATAR POPUP COMPONENT
8 | function EditAvatarPopup({
9 | isOpen,
10 | onClose,
11 | onUpdateAvatar,
12 | onLoading,
13 | onOverlayClick,
14 | }) {
15 | // VALIDATION CUSTOM HOOK
16 | const { values, errors, isFormValid, onChange, resetValidation } = useValidation();
17 | // RESET INPUT VALUE
18 | useEffect(() => {
19 | resetValidation();
20 | }, [isOpen, resetValidation]);
21 | // HANDLE SUBMIT
22 | function handleSubmit(e) {
23 | e.preventDefault();
24 | onUpdateAvatar(values);
25 | }
26 | return (
27 |
37 |
59 |
60 | );
61 | }
62 |
63 | export default EditAvatarPopup;
64 |
--------------------------------------------------------------------------------
/frontend/src/components/Main.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | // IMPORT COMPONENTS
4 | import Card from "./Card";
5 |
6 | // IMPORT CONTEXT
7 | import { CurrentUserContext } from "../contexts/CurrentUserContext";
8 |
9 | // MAIN COMPONENT
10 | function Main({
11 | cards,
12 | onEditAvatar,
13 | onEditProfile,
14 | onAddPlace,
15 | onCardClick,
16 | onCardLike,
17 | onCardDelete,
18 | }) {
19 | // CONTEXT VARIABLES
20 | const currentUser = useContext(CurrentUserContext);
21 | return (
22 |
23 |
24 |
25 |
32 |
33 |
34 |
{currentUser.name}
35 |
40 |
41 |
{currentUser.about}
42 |
43 |
44 |
49 |
50 |
51 |
52 | {cards.map((card) => (
53 |
60 | ))}
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | export default Main;
68 |
--------------------------------------------------------------------------------
/frontend/src/utils/api.js:
--------------------------------------------------------------------------------
1 | // VARIABLES
2 | const BASE_URL = "https://api.mesto.ld-webdev.ru";
3 |
4 | // MAKE REQUEST TO THE SERVER
5 | function makeRequest(url, method, body) {
6 | const headers = { "Content-Type": "application/json" };
7 | const config = { method, headers, credentials: "include" };
8 | if (body !== undefined) {
9 | config.body = JSON.stringify(body);
10 | }
11 | return fetch(`${BASE_URL}${url}`, config).then((res) => {
12 | return res.ok
13 | ? res.json()
14 | : Promise.reject(`Ошибка: ${res.status} ${res.statusText}`);
15 | });
16 | }
17 |
18 | // REGISTRATION USER
19 | export function register({ password, email }) {
20 | return makeRequest("/signup", "POST", { password, email });
21 | }
22 |
23 | // AUTHORIZATION USER
24 | export function authorize({ password, email }) {
25 | return makeRequest("/signin", "POST", { password, email });
26 | }
27 |
28 | // LOGOUT USER
29 | export function logout() {
30 | return makeRequest("/users/me", "DELETE");
31 | }
32 |
33 | // GET USER CONTENT FROM THE SERVER
34 | export function getContent() {
35 | return makeRequest("/users/me", "GET");
36 | }
37 |
38 | // GET USER INFO
39 | export function getUserInfo() {
40 | return makeRequest("/users/me", "GET");
41 | }
42 |
43 | // SEND USER INFO
44 | export function setUserInfo({ name, about }) {
45 | return makeRequest("/users/me", "PATCH", { name, about });
46 | }
47 |
48 | // SET USER AVATAR
49 | export function setUserAvatar({ avatar }) {
50 | return makeRequest("/users/me/avatar", "PATCH", { avatar });
51 | }
52 |
53 | // GET INITIAL CARDS
54 | export function getInitialCards() {
55 | return makeRequest("/cards", "GET");
56 | }
57 |
58 | // SEND NEW CARD INFO
59 | export function sendNewCardInfo({ name, link }) {
60 | return makeRequest("/cards", "POST", { name, link });
61 | }
62 |
63 | // DELETE CARD
64 | export function deleteCard(id) {
65 | return makeRequest(`/cards/${id}`, "DELETE");
66 | }
67 |
68 | // CHANGE LIKE CARD STATUS
69 | export function changeLikeCardStatus(id, isLiked) {
70 | let method;
71 | isLiked ? (method = "DELETE") : (method = "PUT");
72 | return makeRequest(`/cards/${id}/likes`, method);
73 | }
74 |
--------------------------------------------------------------------------------
/frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import useValidation from "../utils/useValidation";
2 |
3 | // IMPORT COMPONENTS
4 | import AuthScreen from "./AuthScreen";
5 |
6 | // LOGIN COMPONENT
7 | function Login({ onLogin, onLoading }) {
8 | // VALIDATION CUSTOM HOOK
9 | const { values, errors, isFormValid, onChange } = useValidation();
10 | // HANDLE SUBMIT
11 | function handleSubmit(e) {
12 | e.preventDefault();
13 | onLogin(values);
14 | }
15 | return (
16 |
23 |
45 |
68 |
69 | );
70 | }
71 |
72 | export default Login;
73 |
--------------------------------------------------------------------------------
/frontend/src/components/Register.js:
--------------------------------------------------------------------------------
1 | import useValidation from "../utils/useValidation";
2 |
3 | // IMPORT COMPONENTS
4 | import AuthScreen from "./AuthScreen";
5 |
6 | // LOGIN COMPONENT
7 | function Register({ onRegistr, onLoading }) {
8 | // VALIDATION CUSTOM HOOK
9 | const { values, errors, isFormValid, onChange } = useValidation();
10 | // HANDLE SUBMIT
11 | function handleSubmit(e) {
12 | e.preventDefault();
13 | onRegistr(values);
14 | }
15 | return (
16 |
23 |
45 |
68 |
69 | );
70 | }
71 |
72 | export default Register;
73 |
--------------------------------------------------------------------------------
/backend/models/user.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const mongoose = require('mongoose');
3 | const isEmail = require('validator/lib/isEmail');
4 | const bcrypt = require('bcryptjs');
5 |
6 | // IMPORT ERRORS
7 | const AuthorizationError = require('../errors/authorizationError');
8 |
9 | // IMPORT VARIABLES
10 | const { LINK_REGEXP } = require('../utils/constants');
11 |
12 | // USER SCHEMA
13 | const userSchema = new mongoose.Schema({
14 | name: {
15 | type: String,
16 | required: [true, 'Поле "name" должно быть заполнено'],
17 | minlength: [2, 'Минимальная длина поля "name" 2 символа'],
18 | maxlength: [30, 'Максимальная длина поля "name" 30 символов'],
19 | default: 'Жак-Ив Кусто',
20 | },
21 | about: {
22 | type: String,
23 | required: [true, 'Поле "about" должно быть заполнено'],
24 | minlength: [2, 'Минимальная длина поля "about" 2 символа'],
25 | maxlength: [30, 'Максимальная длина поля "about" 30 символов'],
26 | default: 'Исследователь',
27 | },
28 | avatar: {
29 | type: String,
30 | required: [true, 'Поле "avatar" должно быть заполнено'],
31 | validate: {
32 | validator: (v) => LINK_REGEXP.test(v),
33 | message: 'Неправильный формат ссылки',
34 | },
35 | default: 'https://pictures.s3.yandex.net/resources/jacques-cousteau_1604399756.png',
36 | },
37 | email: {
38 | type: String,
39 | unique: true,
40 | required: [true, 'Поле "email" должно быть заполнено'],
41 | validate: {
42 | validator: (v) => isEmail(v),
43 | message: 'Неправильный формат почты',
44 | },
45 | },
46 | password: {
47 | type: String,
48 | required: [true, 'Поле "password" должно быть заполнено'],
49 | select: false,
50 | },
51 | }, {
52 | versionKey: false,
53 | // FIND USER BY CREDENTIALS METHOD
54 | statics: {
55 | findUserByCredentials(email, password) {
56 | return this.findOne({ email }).select('+password')
57 | .then((user) => {
58 | if (!user) {
59 | throw new AuthorizationError('Неправильная почта или пароль');
60 | }
61 | return bcrypt.compare(password, user.password)
62 | .then((matched) => {
63 | if (!matched) {
64 | throw new AuthorizationError('Неправильная почта или пароль');
65 | }
66 | return user;
67 | });
68 | });
69 | },
70 | },
71 | });
72 |
73 | // MODULE EXPORT
74 | module.exports = mongoose.model('user', userSchema);
75 |
--------------------------------------------------------------------------------
/frontend/src/components/AddPlacePopup.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import useValidation from "../utils/useValidation";
3 |
4 | // IMPORT COMPONENTS
5 | import PopupWithForm from "./PopupWithForm";
6 |
7 | // ADD PLACE POPUP COMPONENT
8 | function AddPlacePopup({
9 | isOpen,
10 | onClose,
11 | onAddPlace,
12 | onLoading,
13 | onOverlayClick,
14 | }) {
15 | // VALIDATION CUSTOM HOOK
16 | const { values, errors, isFormValid, onChange, resetValidation } = useValidation();
17 | // RESET INPUTS VALUE
18 | useEffect(() => {
19 | resetValidation();
20 | }, [isOpen, resetValidation]);
21 | // HANDLE SUBMIT
22 | function handleSubmit(e) {
23 | e.preventDefault();
24 | onAddPlace(values);
25 | }
26 | return (
27 |
37 |
61 |
83 |
84 | );
85 | }
86 |
87 | export default AddPlacePopup;
88 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests 15 sprint
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | tags:
8 | - '**'
9 |
10 |
11 | jobs:
12 | test_config:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Set up GitHub Actions
16 | uses: actions/checkout@v3
17 | - name: Use Node.js 14.x
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: 14.x
21 | - name: Сheck if the repository is private
22 | run: exit 1
23 | if: ${{github.event.repository.private}}
24 | - name: Get testing lib
25 | run: set -eu && git clone --branch master --single-branch --depth 1 https://github.com/Yandex-Practicum/web-autotest-public.git
26 | - name: Copy Makefile
27 | run: cp ./web-autotest-public/Makefile ./Makefile
28 | - name: Installing Dependencies
29 | run: cd backend&&npm i
30 | - name: Cache test dependencies
31 | id: cache
32 | uses: actions/cache@v3
33 | with:
34 | path: ./web-autotest-public
35 | key: modules-${{ hashFiles('**/pnpm-lock.yaml') }}
36 | - name: Install test dependencies
37 | if: steps.cache.outputs.cache-hit != 'true'
38 | run: |
39 | npm install -g pnpm@7.30.3
40 | pnpm i --fix-lockfile --prefix web-autotest-public/
41 | - name: Run test config
42 | run: make proj15-test-config
43 | test_endpoints:
44 | runs-on: ubuntu-latest
45 | steps:
46 | - name: Set up GitHub Actions
47 | uses: actions/checkout@v3
48 | - name: Use Node.js 14.x
49 | uses: actions/setup-node@v3
50 | with:
51 | node-version: 14.x
52 | - name: Сheck if the repository is private
53 | run: exit 1
54 | if: ${{github.event.repository.private}}
55 | - name: Get testing lib
56 | run: set -eu && git clone --branch master --single-branch --depth 1 https://github.com/Yandex-Practicum/web-autotest-public.git
57 | - name: Copy Makefile
58 | run: cp ./web-autotest-public/Makefile ./Makefile
59 | - name: Start MongoDB
60 | uses: supercharge/mongodb-github-action@1.6.0
61 | with:
62 | mongodb-version: '4.4'
63 | - name: Installing Dependencies
64 | run: cd backend&&npm i
65 | - name: Installing wait-port
66 | run: npm install -g wait-port
67 | - name: Run server
68 | run: npm --prefix backend/ run start & wait-port -t 30000 localhost:3000
69 | - name: Cache test dependencies
70 | id: cache
71 | uses: actions/cache@v3
72 | with:
73 | path: ./web-autotest-public
74 | key: modules-${{ hashFiles('**/pnpm-lock.yaml') }}
75 | - name: Install test dependencies
76 | if: steps.cache.outputs.cache-hit != 'true'
77 | run: |
78 | npm install -g pnpm@7.30.3
79 | pnpm i --fix-lockfile --prefix web-autotest-public/
80 | - name: Run test endpoints
81 | run: make proj15-test-endpoints
82 |
--------------------------------------------------------------------------------
/frontend/src/components/EditProfilePopup.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useContext } from "react";
2 | import useValidation from "../utils/useValidation";
3 |
4 | // IMPORT COMPONENTS
5 | import PopupWithForm from "./PopupWithForm";
6 |
7 | // IMPORT CONTEXT
8 | import { CurrentUserContext } from "../contexts/CurrentUserContext";
9 |
10 | // EDIT PROFILE POPUP COMPONENT
11 | function EditProfilePopup({
12 | isOpen,
13 | onClose,
14 | onUpdateUser,
15 | onLoading,
16 | onOverlayClick,
17 | }) {
18 | // VALIDATION CUSTOM HOOK
19 | const { values, errors, isFormValid, onChange, resetValidation } = useValidation();
20 | // CONTEXT VARIABLES
21 | const currentUser = useContext(CurrentUserContext);
22 | // SET USER DATA TO INPUTS FROM PROFILE
23 | useEffect(() => {
24 | resetValidation(true, currentUser);
25 | }, [currentUser, isOpen, resetValidation]);
26 | // HANDLE SUBMIT
27 | function handleSubmit(e) {
28 | e.preventDefault();
29 | onUpdateUser(values);
30 | }
31 | return (
32 |
42 |
66 |
90 |
91 | );
92 | }
93 |
94 | export default EditProfilePopup;
95 |
--------------------------------------------------------------------------------
/backend/controllers/cards.js:
--------------------------------------------------------------------------------
1 | // IMPORT ERRORS
2 | const {
3 | ValidationError,
4 | DocumentNotFoundError,
5 | CastError,
6 | } = require('mongoose').Error;
7 | const ForbiddenError = require('../errors/forbiddenError');
8 | const NotFoundError = require('../errors/notFoundError');
9 | const IncorrectDataError = require('../errors/incorrectDataError');
10 |
11 | // IMPORT MODELS
12 | const Card = require('../models/card');
13 |
14 | // IMPORT VARIABLES
15 | const { CREATE_CODE } = require('../utils/constants');
16 |
17 | // GET ALL CARDS
18 | module.exports.getAllCards = (req, res, next) => {
19 | Card.find({})
20 | .then((cards) => res.send(cards))
21 | .catch(next);
22 | };
23 |
24 | // CREATE CARD
25 | module.exports.createCard = (req, res, next) => {
26 | const { name, link } = req.body;
27 | Card.create({ name, link, owner: req.user._id })
28 | .then((card) => res.status(CREATE_CODE).send(card))
29 | .catch((err) => {
30 | if (err instanceof ValidationError) {
31 | next(new IncorrectDataError('Переданы некорректные данные для создания карточки.'));
32 | } else {
33 | next(err);
34 | }
35 | });
36 | };
37 |
38 | // DELETE CARD
39 | module.exports.deleteCard = (req, res, next) => {
40 | Card.findById(req.params.cardId)
41 | .orFail()
42 | .then((card) => {
43 | Card.deleteOne({ _id: card._id, owner: req.user._id })
44 | .then((result) => {
45 | if (result.deletedCount === 0) {
46 | throw new ForbiddenError(`Карточка с id ${req.params.cardId} не принадлежит пользователю с id ${req.user._id}`);
47 | }
48 | res.send({ message: 'Пост удалён' });
49 | })
50 | .catch(next);
51 | })
52 | .catch((err) => {
53 | if (err instanceof DocumentNotFoundError) {
54 | next(new NotFoundError(`В базе данных не найдена карточка с ID: ${req.params.cardId}.`));
55 | } else if (err instanceof CastError) {
56 | next(new IncorrectDataError(`Передан некорректный ID карточки: ${req.params.cardId}.`));
57 | } else {
58 | next(err);
59 | }
60 | });
61 | };
62 |
63 | // CARD LIKES UPDATE COMMON METHOD
64 | const cardLikesUpdate = (req, res, updateData, next) => {
65 | Card.findByIdAndUpdate(req.params.cardId, updateData, { new: true })
66 | .orFail()
67 | .then((card) => res.send(card))
68 | .catch((err) => {
69 | if (err instanceof DocumentNotFoundError) {
70 | next(new NotFoundError(`В базе данных не найдена карточка с ID: ${req.params.cardId}.`));
71 | } else if (err instanceof CastError) {
72 | next(new IncorrectDataError(`Передан некорректный ID карточки: ${req.params.cardId}.`));
73 | } else {
74 | next(err);
75 | }
76 | });
77 | };
78 |
79 | // LIKE CARD
80 | module.exports.likeCard = (req, res, next) => {
81 | const updateData = { $addToSet: { likes: req.user._id } };
82 | cardLikesUpdate(req, res, updateData, next);
83 | };
84 |
85 | // DISLIKE CARD
86 | module.exports.dislikeCard = (req, res, next) => {
87 | const updateData = { $pull: { likes: req.user._id } };
88 | cardLikesUpdate(req, res, updateData, next);
89 | };
90 |
--------------------------------------------------------------------------------
/frontend/src/images/logo-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/images/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/controllers/users.js:
--------------------------------------------------------------------------------
1 | // IMPORT PACKAGES
2 | const bcrypt = require('bcryptjs');
3 | const jwt = require('jsonwebtoken');
4 |
5 | // IMPORT ERRORS
6 | const {
7 | ValidationError,
8 | DocumentNotFoundError,
9 | CastError,
10 | } = require('mongoose').Error;
11 | const NotFoundError = require('../errors/notFoundError');
12 | const IncorrectDataError = require('../errors/incorrectDataError');
13 | const ConflictError = require('../errors/conflictError');
14 |
15 | // IMPORT MODELS
16 | const User = require('../models/user');
17 |
18 | // CONFIG VARIABLES
19 | const { NODE_ENV, SECRET_KEY } = process.env;
20 | const { MODE_PRODUCTION, DEV_KEY } = require('../utils/config');
21 |
22 | // IMPORT VARIABLES
23 | const { CREATE_CODE } = require('../utils/constants');
24 |
25 | // GET ALL USERS
26 | module.exports.getAllUsers = (req, res, next) => {
27 | User.find({})
28 | .then((users) => res.send(users))
29 | .catch(next);
30 | };
31 |
32 | // FIND USER BY ID COMMON METHOD
33 | const findUserById = (req, res, requiredData, next) => {
34 | User.findById(requiredData)
35 | .orFail()
36 | .then((user) => res.send(user))
37 | .catch((err) => {
38 | if (err instanceof DocumentNotFoundError) {
39 | next(new NotFoundError(`В базе данных не найден пользователь с ID: ${requiredData}.`));
40 | } else if (err instanceof CastError) {
41 | next(new IncorrectDataError(`Передан некорректный ID пользователя: ${requiredData}.`));
42 | } else {
43 | next(err);
44 | }
45 | });
46 | };
47 |
48 | // GET USER
49 | module.exports.getUser = (req, res, next) => {
50 | findUserById(req, res, req.params.userId, next);
51 | };
52 |
53 | // GET USER INFO
54 | module.exports.getUserInfo = (req, res, next) => {
55 | findUserById(req, res, req.user._id, next);
56 | };
57 |
58 | // CREATE USER
59 | module.exports.createUser = (req, res, next) => {
60 | const {
61 | name,
62 | about,
63 | avatar,
64 | email,
65 | password,
66 | } = req.body;
67 | bcrypt.hash(password, 10)
68 | .then((hash) => User.create({
69 | name,
70 | about,
71 | avatar,
72 | email,
73 | password: hash,
74 | }))
75 | .then((user) => {
76 | const data = user.toObject();
77 | delete data.password;
78 | res.status(CREATE_CODE).send(data);
79 | })
80 | .catch((err) => {
81 | if (err instanceof ValidationError) {
82 | next(new IncorrectDataError('Переданы некорректные данные для создания пользователя.'));
83 | } else if (err.code === 11000) {
84 | next(new ConflictError('Указанный email уже зарегистрирован. Пожалуйста используйте другой email'));
85 | } else {
86 | next(err);
87 | }
88 | });
89 | };
90 |
91 | // USER UPDATE COMMON METHOD
92 | const userUpdate = (req, res, updateData, next) => {
93 | User.findByIdAndUpdate(req.user._id, updateData, { new: true, runValidators: true })
94 | .orFail()
95 | .then((user) => res.send(user))
96 | .catch((err) => {
97 | if (err instanceof DocumentNotFoundError) {
98 | next(new NotFoundError(`В базе данных не найден пользователь с ID: ${req.user._id}.`));
99 | } else if (err instanceof CastError) {
100 | next(new IncorrectDataError(`Передан некорректный ID пользователя: ${req.user._id}.`));
101 | } else if (err instanceof ValidationError) {
102 | next(new IncorrectDataError('Переданы некорректные данные для редактирования профиля.'));
103 | } else {
104 | next(err);
105 | }
106 | });
107 | };
108 |
109 | // UPDATE USER INFO
110 | module.exports.updateUserInfo = (req, res, next) => {
111 | const { name, about } = req.body;
112 | userUpdate(req, res, { name, about }, next);
113 | };
114 |
115 | // UPDATE USER AVATAR
116 | module.exports.updateUserAvatar = (req, res, next) => {
117 | const { avatar } = req.body;
118 | userUpdate(req, res, { avatar }, next);
119 | };
120 |
121 | // LOGIN
122 | module.exports.login = (req, res, next) => {
123 | const { email, password } = req.body;
124 | return User.findUserByCredentials(email, password)
125 | .then((user) => {
126 | const token = jwt.sign(
127 | { _id: user._id },
128 | NODE_ENV === MODE_PRODUCTION ? SECRET_KEY : DEV_KEY,
129 | { expiresIn: '7d' },
130 | );
131 | res.cookie('jwt', token, {
132 | maxAge: 3600000 * 24 * 7,
133 | httpOnly: true,
134 | sameSite: true,
135 | });
136 | res.send({ message: 'Успешный вход' });
137 | })
138 | .catch(next);
139 | };
140 |
141 | // LOGOUT
142 | module.exports.logout = (req, res) => {
143 | res.cookie('jwt', 'none', {
144 | maxAge: 5000,
145 | httpOnly: true,
146 | sameSite: true,
147 | });
148 | res.send({ message: 'Успешный выход' });
149 | };
150 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | /* VENDOR */
2 | @import url("./vendor/normalize.css");
3 | @import url("./vendor/font.css");
4 |
5 | /* PAGE */
6 | @import url("./blocks/page/page.css");
7 | @import url("./blocks/page/__content/page__content.css");
8 |
9 | /* CONTENT */
10 | @import url("./blocks/content/content.css");
11 |
12 | /* HAMBURGER */
13 | @import url("./blocks/hamburger/hamburger.css");
14 | @import url("./blocks/hamburger/_active/hamburger_active.css");
15 | @import url("./blocks/hamburger/__user-email/hamburger__user-email.css");
16 | @import url("./blocks/hamburger/__btn-sign-out/hamburger__btn-sign-out.css");
17 |
18 | /* HEADER */
19 | @import url("./blocks/header/header.css");
20 | @import url("./blocks/header/__logo/header__logo.css");
21 | @import url("./blocks/header/__link/header__link.css");
22 | @import url("./blocks/header/__nav/header__nav.css");
23 | @import url("./blocks/header/__btn-sign-out/header__btn-sign-out.css");
24 | @import url("./blocks/header/__user-email/header__user-email.css");
25 | @import url("./blocks/header/__btn-hamburger/header__btn-hamburger.css");
26 | @import url("./blocks/header/__btn-hamburger/_type/header__btn-hamburger_type_close.css");
27 |
28 | /* LOGO */
29 | @import url("./blocks/logo/logo.css");
30 |
31 | /* PROFILE */
32 | @import url("./blocks/profile/profile.css");
33 | @import url("./blocks/profile/__wrapper/profile__wrapper.css");
34 | @import url("./blocks/profile/__btn-avatar-edit/profile__btn-avatar-edit.css");
35 | @import url("./blocks/profile/__avatar/profile__avatar.css");
36 | @import url("./blocks/profile/__user-wrapper/profile__user-wrapper.css");
37 | @import url("./blocks/profile/__user-edit/profile__user-edit.css");
38 | @import url("./blocks/profile/__user-name/profile__user-name.css");
39 | @import url("./blocks/profile/__btn-edit/profile__btn-edit.css");
40 | @import url("./blocks/profile/__user-about/profile__user-about.css");
41 | @import url("./blocks/profile/__btn-add/profile__btn-add.css");
42 |
43 | /* CARDS */
44 | @import url("./blocks/cards/cards.css");
45 | @import url("./blocks/cards/__wrapper/cards__wrapper.css");
46 |
47 | /* CARD */
48 | @import url("./blocks/card/card.css");
49 | @import url("./blocks/card/__img/card__img.css");
50 | @import url("./blocks/card/__btn-del/card__btn-del.css");
51 | @import url("./blocks/card/__caption/card__caption.css");
52 | @import url("./blocks/card/__title/card__title.css");
53 | @import url("./blocks/card/__like-wrapper/card__like-wrapper.css");
54 | @import url("./blocks/card/__btn-like/card__btn-like.css");
55 | @import url("./blocks/card/__btn-like/_active/card__btn-like_active.css");
56 | @import url("./blocks/card/__like-counter/card__like-counter.css");
57 |
58 | /* FOOTER */
59 | @import url("./blocks/footer/footer.css");
60 | @import url("./blocks/footer/__copyright/footer__copyright.css");
61 |
62 | /* POPUP */
63 | @import url("./blocks/popup/popup.css");
64 | @import url("./blocks/popup/_opened/popup_opened.css");
65 | @import url("./blocks/popup/_type/popup_type_img.css");
66 | @import url("./blocks/popup/__container/popup__container.css");
67 | @import url("./blocks/popup/__container/_type/popup__container_type_form.css");
68 | @import url("./blocks/popup/__figure-wrapper/popup__figure-wrapper.css");
69 | @import url("./blocks/popup/__img/popup__img.css");
70 | @import url("./blocks/popup/__img-caption/popup__img-caption.css");
71 | @import url("./blocks/popup/__title/popup__title.css");
72 | @import url("./blocks/popup/__btn-close/popup__btn-close.css");
73 | @import url("./blocks/popup/__status-wrapper/popup__status-wrapper.css");
74 | @import url("./blocks/popup/__status-icon/popup__status-icon.css");
75 | @import url("./blocks/popup/__status-icon/_type/popup__status-icon_type_fail.css");
76 | @import url("./blocks/popup/__status-icon/_type/popup__status-icon_type_success.css");
77 | @import url("./blocks/popup/__status-text/popup__status-text.css");
78 |
79 | /* PRELOADER */
80 | @import url("./blocks/preloader/preloader.css");
81 | @import url("./blocks/preloader/_active/preloader_active.css");
82 |
83 | /* AUTHORIZATION */
84 | @import url("./blocks/authorization/authorization.css");
85 | @import url("./blocks/authorization/__wrapper/authorization__wrapper.css");
86 | @import url("./blocks/authorization/__title/authorization__title.css");
87 | @import url("./blocks/authorization/__text/authorization__text.css");
88 | @import url("./blocks/authorization/__link/authorization__link.css");
89 |
90 | /* FORM */
91 | @import url("./blocks/form/form.css");
92 | @import url("./blocks/form/_place/form_place_authorization.css");
93 | @import url("./blocks/form/_type/form_type_card-delete-confirmation.css");
94 | @import url("./blocks/form/__input/form__input.css");
95 | @import url("./blocks/form/__input/_place/form__input_place_authorization.css");
96 | @import url("./blocks/form/__input/_type/form__input_type_error.css");
97 | @import url("./blocks/form/__input-error/form__input-error.css");
98 | @import url("./blocks/form/__input-error/_active/form__input-error_active.css");
99 | @import url("./blocks/form/__btn-submit/form__btn-submit.css");
100 | @import url("./blocks/form/__btn-submit/_place/form__btn-submit_place_authorization.css");
101 |
102 | /* NOT FOUND */
103 | @import url("./blocks/not-found/not-found.css");
104 | @import url("./blocks/not-found/__title/not-found__title.css");
105 | @import url("./blocks/not-found/__description/not-found__description.css");
106 | @import url("./blocks/not-found/__link/not-found__link.css");
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Проект: Место (frontend + backend)
2 |
3 | Учебный проект выполненный в рамках курса "Веб-разработчик" от Яндекс Практикум.
4 | Проект представляет из себя веб-приложение в сборе, имеющие бэкенд часть, основой для которой послужил [проект](https://github.com/Bjorn86/express-mesto-gha) также выполненный в рамках учебного курса, а также фронтенд часть, основой для которого послужил ещё один [проект](https://github.com/Bjorn86/react-mesto-auth) выполненный в рамках указанного выше учебного курса.
5 |
6 | ## Оглавление
7 |
8 | - [Обзор проекта](#обзор-проекта)
9 | - [Задачи проекта](#задачи-проекта)
10 | - [Функциональность проекта](#функциональность-проекта)
11 | - [Screenshot](#screenshot)
12 | - [Директории проекта](#директории-проекта)
13 | - [Запуск проекта](#запуск-проекта)
14 | - [Ссылки на проект](#ссылки-на-проект)
15 | - [Ход выполнения проекта](#ход-выполнения-проекта)
16 | - [Используемые технологии](#используемые-технологии)
17 | - [Чему я научился работая над проектом](#чему-я-научился-работая-над-проектом)
18 | - [Автор](#автор)
19 |
20 | ## Обзор проекта
21 |
22 | ### Задачи проекта
23 |
24 | Проект призван закрепить вре ранее полученные в рамках учебного курса знания. Создать полностью рабочее веб-приложение, и разместить его на хостинге.
25 |
26 | ### Функциональность проекта
27 |
28 | - Backend:
29 | - В проекте созданы схемы и модели пользователей и карточек с контентом:
30 | - `card` — схема карточки с контентом
31 | - `user` — схема пользователя
32 | - В проекте созданы эндпоинты:
33 | - `/cards` — обрабатывает:
34 | - GET запросы — отдаёт все карточки из БД
35 | - POST запросы — создаёт новую карточку с контентом
36 | - `/cards/:cardId` — обрабатывает DELETE запросы, удаляет карточку по `cardId`
37 | - `/cards/:cardId/likes` — обрабатывает:
38 | - PUT запросы — добавляет лайк карточке с контентом
39 | - DELETE запросы — удаляет лайк карточке с контентом
40 | - `/signin` — обрабатывает POST запросы, производит аутентификацию пользователя
41 | - `/signup` — обрабатывает POST запросы, производит регистрацию пользователя
42 | - `/users` — обрабатывает:
43 | - GET запросы — отдаёт всех пользователей из БД
44 | - POST запросы — создаёт нового пользователя
45 | - `/users/:userId` — обрабатывает GET запросы, отдаёт пользователя по `userId`
46 | - `/users/me` — обрабатывает:
47 | - GET запросы — отдаёт информацию о текущем пользователе
48 | - PATCH запросы — обновляет информацию о пользователе
49 | - DELETE запросы — производит выход пользователя, с удалением JWT-токена из Cookie
50 | - `/users/me/avatar` — обрабатывает PATCH запросы, обновляет аватар пользователя
51 | - Созданы мидлвары:
52 | - Централизованной обработки ошибок
53 | - Авторизации пользователя
54 | - Ограничитель количества запросов (защита от DDoS атак)
55 | - Поддержки CORS запросов, включая обработку предварительных запросов
56 | - Логирования запросов и ошибок
57 | - Производится валидация поступающих данных:
58 | - до передачи информации контроллерам с помощью joi и celebrate
59 | - на уровне схем с помощью validator и встроенных методов mongoose
60 | - Frontend:
61 | - Возможность регистрации и аутентификации пользователя
62 | - Возможность редактировать информацию о пользователе (установить имя пользователя, информацию «о себе», аватар)
63 | - Возможность создавать карточки мест (добавить\удалить карточку места, поставить\снять лайк карточке)
64 | - Возможность просматривать детальную фотографию карточки
65 | - Реализована валидация форм с помощью кастомного хука
66 |
67 | ### Screenshot
68 |
69 | 
70 |
71 | ### Директории проекта
72 |
73 | - `/backend` — директория с файлами бэкенд части проекта
74 | - `/controllers` — директория с файлами контроллеров
75 | - `/errors` — директория с файлами кастомных ошибок
76 | - `/middlewares` — директория с мидлварами
77 | - `/models` — директория с файлами описания схем и моделей
78 | - `/routes` — директория с файлами роутера
79 | - `/utils` — директория со вспомогательными файлами
80 | - `/frontend` — директория с файлами фронтенд части проекта
81 | - `src/blocks` — директория с CSS файлами
82 | - `src/components` — директория с компонентами
83 | - `src/contexts` — директория с элементами контекста
84 | - `src/fonts` — директория со шрифтами
85 | - `src/images` — директория с файлами изображений
86 | - `src/utils` — директория со вспомогательными файлами
87 | - `src/vendor` — директория с файлами библиотек
88 |
89 | ### Запуск проекта
90 |
91 | - Backend:
92 | - `npm lint` — запускает проверку линтером
93 | - `npm run start` — запускает сервер
94 | - `npm run dev` — запускает сервер с hot-reload
95 | - Frontend:
96 | - `npm run build` — запуск проекта в режиме продакшн, с формированием файлов подготовленных к деплою в директории `/build`
97 | - `npm start` — запуск проекта в режиме разработки
98 |
99 |
100 |
101 | ### Ссылки на проект
102 |
103 | - [Ссылка на репозиторий проекта](https://github.com/Bjorn86/react-mesto-api-full-gha)
104 | - [Ссылка на страницу проекта](https://mesto.ld-webdev.ru)
105 | - [Ссылка на API сервер проекта](https://api.mesto.ld-webdev.ru)
106 |
107 | ## Ход выполнения проекта
108 |
109 | ### Используемые технологии
110 |
111 | - [Node.js](https://nodejs.org/ru)
112 | - [nodemon](https://nodemon.io/)
113 | - [Express](https://expressjs.com/)
114 | - [cookie-parser](https://www.npmjs.com/package/cookie-parser)
115 | - [MongoDB](https://www.mongodb.com/)
116 | - [mongoose](https://mongoosejs.com/)
117 | - [bcryptjs](https://www.npmjs.com/package/bcryptjs)
118 | - [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)
119 | - [celebrate](https://www.npmjs.com/package/celebrate)
120 | - [validator](https://www.npmjs.com/package/validator)
121 | - [express-rate-limit](https://www.npmjs.com/package/express-rate-limit)
122 | - [helmet](https://helmetjs.github.io/)
123 | - [winston](https://www.npmjs.com/package/winston)
124 | - [express-winston](https://www.npmjs.com/package/express-winston)
125 | - [ESLint](https://eslint.org/)
126 |
127 | ### Чему я научился работая над проектом
128 |
129 | - Разворачивать сервер на Node.js
130 | - Использовать в работе фреймворк Express
131 | - Работать с БД MongoDB
132 | - Использовать в работе с БД ODM mongoose
133 | - Создавать схемы и модели для работы с БД
134 | - Обрабатывать различные виды запросов
135 | - Обрабатывать ошибки некорректных запросов
136 | - Валидировать приходящую в запросе информацию
137 | - Работать с JWT-токеном
138 | - Работать с Cookies
139 | - Базовой защите приложения
140 | - Логированию
141 | - Работе с CORS
142 | - Деплою проекта на реальный хостинг
143 |
144 | ## Автор
145 |
146 | **Данила Легкобытов**
147 |
148 | - e-mail: [legkobytov-danila@yandex.ru](mailto:legkobytov-danila@yandex.ru)
149 | - Telegram: [@danila_legkobytov](https://t.me/danila_legkobytov)
150 | - LinkedIn: [in/danila-legkobytov](https://www.linkedin.com/in/danila-legkobytov/)
151 |
--------------------------------------------------------------------------------
/frontend/src/vendor/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input { /* 1 */
178 | overflow: visible;
179 | }
180 |
181 | /**
182 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
183 | * 1. Remove the inheritance of text transform in Firefox.
184 | */
185 |
186 | button,
187 | select { /* 1 */
188 | text-transform: none;
189 | }
190 |
191 | /**
192 | * Correct the inability to style clickable types in iOS and Safari.
193 | */
194 |
195 | button,
196 | [type="button"],
197 | [type="reset"],
198 | [type="submit"] {
199 | -webkit-appearance: button;
200 | }
201 |
202 | /**
203 | * Remove the inner border and padding in Firefox.
204 | */
205 |
206 | button::-moz-focus-inner,
207 | [type="button"]::-moz-focus-inner,
208 | [type="reset"]::-moz-focus-inner,
209 | [type="submit"]::-moz-focus-inner {
210 | border-style: none;
211 | padding: 0;
212 | }
213 |
214 | /**
215 | * Restore the focus styles unset by the previous rule.
216 | */
217 |
218 | button:-moz-focusring,
219 | [type="button"]:-moz-focusring,
220 | [type="reset"]:-moz-focusring,
221 | [type="submit"]:-moz-focusring {
222 | outline: 1px dotted ButtonText;
223 | }
224 |
225 | /**
226 | * Correct the padding in Firefox.
227 | */
228 |
229 | fieldset {
230 | padding: 0.35em 0.75em 0.625em;
231 | }
232 |
233 | /**
234 | * 1. Correct the text wrapping in Edge and IE.
235 | * 2. Correct the color inheritance from `fieldset` elements in IE.
236 | * 3. Remove the padding so developers are not caught out when they zero out
237 | * `fieldset` elements in all browsers.
238 | */
239 |
240 | legend {
241 | box-sizing: border-box; /* 1 */
242 | color: inherit; /* 2 */
243 | display: table; /* 1 */
244 | max-width: 100%; /* 1 */
245 | padding: 0; /* 3 */
246 | white-space: normal; /* 1 */
247 | }
248 |
249 | /**
250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
251 | */
252 |
253 | progress {
254 | vertical-align: baseline;
255 | }
256 |
257 | /**
258 | * Remove the default vertical scrollbar in IE 10+.
259 | */
260 |
261 | textarea {
262 | overflow: auto;
263 | }
264 |
265 | /**
266 | * 1. Add the correct box sizing in IE 10.
267 | * 2. Remove the padding in IE 10.
268 | */
269 |
270 | [type="checkbox"],
271 | [type="radio"] {
272 | box-sizing: border-box; /* 1 */
273 | padding: 0; /* 2 */
274 | }
275 |
276 | /**
277 | * Correct the cursor style of increment and decrement buttons in Chrome.
278 | */
279 |
280 | [type="number"]::-webkit-inner-spin-button,
281 | [type="number"]::-webkit-outer-spin-button {
282 | height: auto;
283 | }
284 |
285 | /**
286 | * 1. Correct the odd appearance in Chrome and Safari.
287 | * 2. Correct the outline style in Safari.
288 | */
289 |
290 | [type="search"] {
291 | -webkit-appearance: textfield; /* 1 */
292 | outline-offset: -2px; /* 2 */
293 | }
294 |
295 | /**
296 | * Remove the inner padding in Chrome and Safari on macOS.
297 | */
298 |
299 | [type="search"]::-webkit-search-decoration {
300 | -webkit-appearance: none;
301 | }
302 |
303 | /**
304 | * 1. Correct the inability to style clickable types in iOS and Safari.
305 | * 2. Change font properties to `inherit` in Safari.
306 | */
307 |
308 | ::-webkit-file-upload-button {
309 | -webkit-appearance: button; /* 1 */
310 | font: inherit; /* 2 */
311 | }
312 |
313 | /* Interactive
314 | ========================================================================== */
315 |
316 | /*
317 | * Add the correct display in Edge, IE 10+, and Firefox.
318 | */
319 |
320 | details {
321 | display: block;
322 | }
323 |
324 | /*
325 | * Add the correct display in all browsers.
326 | */
327 |
328 | summary {
329 | display: list-item;
330 | }
331 |
332 | /* Misc
333 | ========================================================================== */
334 |
335 | /**
336 | * Add the correct display in IE 10+.
337 | */
338 |
339 | template {
340 | display: none;
341 | }
342 |
343 | /**
344 | * Add the correct display in IE 10.
345 | */
346 |
347 | [hidden] {
348 | display: none;
349 | }
350 |
--------------------------------------------------------------------------------
/frontend/src/components/App.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "react";
2 | import { Route, Routes, useNavigate } from "react-router-dom";
3 |
4 | // IMPORT COMPONENTS
5 | import AppLayout from "./AppLayout";
6 | import Login from "./Login";
7 | import Register from "./Register";
8 | import Main from "./Main";
9 | import ImagePopup from "./ImagePopup";
10 | import EditProfilePopup from "./EditProfilePopup";
11 | import EditAvatarPopup from "./EditAvatarPopup";
12 | import AddPlacePopup from "./AddPlacePopup";
13 | import DeleteCardPopup from "./DeleteCardPopup";
14 | import InfoTooltip from "./InfoTooltip";
15 | import Preloader from "./Preloader";
16 | import ProtectedRouteElement from "./ProtectedRoute";
17 | import NotFound from "./NotFound";
18 |
19 | // IMPORT CONTEXT
20 | import { CurrentUserContext } from "../contexts/CurrentUserContext";
21 |
22 | // IMPORT API'S CLASS INSTANCE
23 | import * as api from "../utils/api";
24 |
25 | // APP COMPONENT
26 | function App() {
27 | // STATE VARIABLES WITH HOOKS
28 | const [isEditAvatarPopupOpen, setEditAvatarPopupClass] = useState(false);
29 | const [isEditProfilePopupOpen, setEditProfilePopupClass] = useState(false);
30 | const [isAddPlacePopupOpen, setAddPlacePopupClass] = useState(false);
31 | const [isDeleteCardPopupOpen, setDeleteCardPopupClass] = useState(false);
32 | const [isInfoTooltipPopupOpen, setInfoTooltipPopupClass] = useState(false);
33 | const [isHamburgerOpen, setHamburgerClass] = useState(false);
34 | const [isLoading, setLoading] = useState(false);
35 | const [isPreloaderActive, setPreloaderClass] = useState(true);
36 | const [infoTooltipStatus, setInfoTooltipStatus] = useState("");
37 | const [selectedCard, setSelectedCard] = useState(null);
38 | const [cardToDelete, setCardToDelete] = useState({});
39 | const [currentUser, setCurrentUser] = useState({});
40 | const [cards, setCards] = useState([]);
41 | const [loggedIn, setLoggedIn] = useState(false);
42 | const [userEmail, setUserEmail] = useState("");
43 |
44 | // VARIABLES WITH HOOKS
45 | const navigate = useNavigate();
46 |
47 | // GETTING PRIMARY DATA FROM THE SERVER
48 | useEffect(() => {
49 | loggedIn &&
50 | Promise.all([api.getUserInfo(), api.getInitialCards()])
51 | .then(([userData, cardsData]) => {
52 | setCurrentUser(userData);
53 | setCards(cardsData.reverse());
54 | })
55 | .catch((err) => {
56 | console.log(err);
57 | });
58 | }, [loggedIn]);
59 |
60 | // HANDLE EDIT AVATAR CLICK
61 | const handleEditAvatarClick = useCallback(() => {
62 | setEditAvatarPopupClass(true);
63 | }, []);
64 |
65 | // HANDLE EDIT PROFILE CLICK
66 | const handleEditProfileClick = useCallback(() => {
67 | setEditProfilePopupClass(true);
68 | }, []);
69 |
70 | // HANDLE ADD PLACE CARD CLICK
71 | const handleAddPlaceClick = useCallback(() => {
72 | setAddPlacePopupClass(true);
73 | }, []);
74 |
75 | // HANDLE DELETE CLICK
76 | const handleDeleteClick = useCallback((card) => {
77 | setDeleteCardPopupClass(true);
78 | setCardToDelete(card);
79 | }, []);
80 |
81 | // HANDLE CARD IMAGE CLICK
82 | const handleCardClick = useCallback((card) => {
83 | setSelectedCard(card);
84 | }, []);
85 |
86 | // HANDLE CLOSE ALL POPUPS
87 | const closeAllPopups = useCallback(() => {
88 | setEditAvatarPopupClass(false);
89 | setEditProfilePopupClass(false);
90 | setAddPlacePopupClass(false);
91 | setDeleteCardPopupClass(false);
92 | setInfoTooltipPopupClass(false);
93 | setSelectedCard(null);
94 | setCardToDelete({});
95 | setInfoTooltipStatus("");
96 | }, []);
97 |
98 | // HANDLE HAMBURGER CLICK
99 | const handleHamburgerClick = useCallback(() => {
100 | setHamburgerClass(!isHamburgerOpen);
101 | }, [isHamburgerOpen]);
102 |
103 | // HANDLE CARD LIKE
104 | const handleCardLike = useCallback(
105 | async (card) => {
106 | const isLiked = card.likes.some((item) => item === currentUser._id);
107 | try {
108 | const data = await api.changeLikeCardStatus(card._id, isLiked);
109 | if (data) {
110 | setCards((state) =>
111 | state.map((item) => (item._id === card._id ? data : item))
112 | );
113 | }
114 | } catch (err) {
115 | console.log(err);
116 | }
117 | },
118 | [currentUser._id]
119 | );
120 |
121 | // HANDLE CARD DELETE
122 | const handleCardDelete = useCallback(
123 | async (card) => {
124 | setLoading(true);
125 | try {
126 | const data = await api.deleteCard(card._id);
127 | if (data) {
128 | setCards((state) => state.filter((item) => item._id !== card._id));
129 | closeAllPopups();
130 | }
131 | } catch (err) {
132 | console.log(err);
133 | } finally {
134 | setLoading(false);
135 | }
136 | },
137 | [closeAllPopups]
138 | );
139 |
140 | // HANDLE UPDATE USER
141 | const handleUpdateUser = useCallback(
142 | async (userData) => {
143 | setLoading(true);
144 | try {
145 | const data = await api.setUserInfo(userData);
146 | if (data) {
147 | setCurrentUser(data);
148 | closeAllPopups();
149 | }
150 | } catch (err) {
151 | console.log(err);
152 | } finally {
153 | setLoading(false);
154 | }
155 | },
156 | [closeAllPopups]
157 | );
158 |
159 | // HANDLE UPDATE AVATAR
160 | const handleUpdateAvatar = useCallback(
161 | async (avatarData) => {
162 | setLoading(true);
163 | try {
164 | const data = await api.setUserAvatar(avatarData);
165 | if (data) {
166 | setCurrentUser(data);
167 | closeAllPopups();
168 | }
169 | } catch (err) {
170 | console.log(err);
171 | } finally {
172 | setLoading(false);
173 | }
174 | },
175 | [closeAllPopups]
176 | );
177 |
178 | // HANDLE ADD PLACE CARD
179 | const handleAddPlaceSubmit = useCallback(
180 | async (cardData) => {
181 | setLoading(true);
182 | try {
183 | const data = await api.sendNewCardInfo(cardData);
184 | if (data) {
185 | setCards([data, ...cards]);
186 | closeAllPopups();
187 | }
188 | } catch (err) {
189 | console.log(err);
190 | } finally {
191 | setLoading(false);
192 | }
193 | },
194 | [cards, closeAllPopups]
195 | );
196 |
197 | // HANDLE USER REGISTRATION
198 | const handleUserRegistration = useCallback(
199 | async (userData) => {
200 | setLoading(true);
201 | try {
202 | const data = await api.register(userData);
203 | if (data) {
204 | setInfoTooltipStatus("success");
205 | setInfoTooltipPopupClass(true);
206 | navigate("/sign-in", { replace: true });
207 | }
208 | } catch (err) {
209 | console.error(err);
210 | setInfoTooltipStatus("fail");
211 | setInfoTooltipPopupClass(true);
212 | } finally {
213 | setLoading(false);
214 | }
215 | },
216 | [navigate]
217 | );
218 |
219 | // HANDLE USER AUTHORIZATION
220 | const handleUserAuthorization = useCallback(
221 | async (userData) => {
222 | setLoading(true);
223 | try {
224 | const data = await api.authorize(userData);
225 | if (data) {
226 | setLoggedIn(true);
227 | setUserEmail(userData.email);
228 | navigate("/", { replace: true });
229 | }
230 | } catch (err) {
231 | console.error(err);
232 | setInfoTooltipStatus("fail");
233 | setInfoTooltipPopupClass(true);
234 | } finally {
235 | setLoading(false);
236 | }
237 | },
238 | [navigate]
239 | );
240 |
241 | // USER LOGIN CHECK
242 | const userLoginCheck = useCallback(async () => {
243 | try {
244 | const userData = await api.getContent();
245 | if (!userData) {
246 | throw new Error("Данные пользователя отсутствует");
247 | }
248 | setUserEmail(userData.email);
249 | setLoggedIn(true);
250 | navigate("/", { replace: true });
251 | } catch (err) {
252 | console.error(err);
253 | } finally {
254 | setPreloaderClass(false);
255 | }
256 | }, [navigate]);
257 |
258 | // HANDLE USER LOGOUT
259 | const handleUserLogOut = useCallback(async () => {
260 | try {
261 | const data = await api.logout();
262 | if (data) {
263 | setLoggedIn(false);
264 | setUserEmail("");
265 | setHamburgerClass(false);
266 | navigate("/sign-in", { replace: true });
267 | }
268 | } catch (err) {
269 | console.error(err);
270 | }
271 | }, [navigate]);
272 |
273 | // CHECK USER LOGGED IN
274 | useEffect(() => {
275 | userLoginCheck();
276 | }, [userLoginCheck]);
277 |
278 | // PRELOADER RENDER
279 | if (isPreloaderActive) {
280 | return ;
281 | }
282 |
283 | return (
284 |
285 |
286 |
287 |
296 | }
297 | >
298 |
312 | }
313 | />
314 |
321 | }
322 | />
323 |
330 | }
331 | />
332 | } />
333 |
334 |
335 |
341 |
347 |
353 |
360 |
361 |
366 |
367 |
368 | );
369 | }
370 |
371 | export default App;
372 |
--------------------------------------------------------------------------------