├── .env.template
├── src
├── components
│ ├── Timer
│ │ ├── index.js
│ │ ├── Timer.styled.jsx
│ │ └── Timer.jsx
│ ├── AuthForm
│ │ ├── index.js
│ │ └── AuthForm.styled.jsx
│ ├── BtnSubmit
│ │ ├── index.js
│ │ ├── BtnSubmit.jsx
│ │ └── BtnSubmit.styled.jsx
│ ├── Calendar
│ │ ├── index.js
│ │ └── Calendar.jsx
│ ├── Checkbox
│ │ ├── index.js
│ │ ├── Checkbox.jsx
│ │ └── Checkbox.styled.js
│ ├── ParamsBar
│ │ ├── index.js
│ │ ├── ParamsBar.styled.jsx
│ │ └── ParamsBar.jsx
│ ├── ParamsBtn
│ │ ├── index.js
│ │ ├── ParamsBtn.styled.jsx
│ │ └── ParamsBtn.jsx
│ ├── ParamsForm
│ │ ├── index.js
│ │ └── ParamsForm.styled.jsx
│ ├── Scrollbar
│ │ ├── index.js
│ │ ├── Scrollbar.jsx
│ │ └── Scrollbar.syled.jsx
│ ├── UserCard
│ │ ├── index.js
│ │ ├── UserCard.jsx
│ │ └── UserCard.styled.js
│ ├── UserForm
│ │ ├── index.js
│ │ ├── validationSchema.js
│ │ └── UserForm.styled.jsx
│ ├── AddExerciseForm
│ │ ├── index.js
│ │ └── AddExerciseForm.styled.jsx
│ ├── ParamsBlockСard
│ │ ├── index.js
│ │ └── ParamsBlockCard.jsx
│ ├── ProductsFilter
│ │ ├── index.js
│ │ └── ProductsFilter.styled.jsx
│ ├── ButtonIconForInput
│ │ ├── index.js
│ │ ├── ButtonIconForInput.jsx
│ │ └── ButtonIconForInput.styled.jsx
│ ├── ProductOrExerciseContainer
│ │ ├── index.js
│ │ ├── ProductOrExerciseContainer.styled.jsx
│ │ └── ProductOrExerciseContainer.jsx
│ ├── Routes
│ │ ├── index.js
│ │ ├── PublicRoute.jsx
│ │ └── PrivateRoute.jsx
│ ├── SubTitle
│ │ ├── SubTitle.jsx
│ │ └── SubTitle.styled.jsx
│ ├── LoaderForPages
│ │ ├── LoaderForPages.styled.jsx
│ │ └── LoaderForPages.jsx
│ ├── Title
│ │ ├── Title.jsx
│ │ └── Title.styled.jsx
│ ├── ExercisesItemList
│ │ ├── ExercisesItemList.styled.jsx
│ │ └── ExercisesItemList.jsx
│ ├── SharedLayout
│ │ ├── SheradLayout.styled.jsx
│ │ └── SharedLayout.jsx
│ ├── MainTitle
│ │ ├── MainTitle.jsx
│ │ └── MainTitle.styled.jsx
│ ├── LinkSubText
│ │ ├── LinkSubText.jsx
│ │ └── LinkSubText.styled.jsx
│ ├── DiaryStatisticsList
│ │ └── DiaryStatisticsList.styled.jsx
│ ├── headersComp
│ │ ├── Logo
│ │ │ ├── Logo.styled.js
│ │ │ └── Logo.jsx
│ │ ├── Header
│ │ │ ├── Header.styled.js
│ │ │ └── Header.jsx
│ │ └── UserNav
│ │ │ ├── UserNav.styled.js
│ │ │ └── UserNav.jsx
│ ├── CustomNavLink
│ │ ├── CustomNavLink.jsx
│ │ └── CustomNavLink.styled.jsx
│ ├── Lodaer
│ │ └── Loader.jsx
│ ├── BtnSubtitle
│ │ ├── BtnSubtitle.jsx
│ │ └── BtnSubtitle.styled.jsx
│ ├── CustomInputForCalendar
│ │ ├── CustomInputForCalendar.jsx
│ │ └── CustomInputForCalendar.styled.jsx
│ ├── EmptyProductList
│ │ ├── EmptyProductList.jsx
│ │ └── EmptyProductList.styled.jsx
│ ├── DescriptionText
│ │ ├── DescriptionText.jsx
│ │ └── DescriptionText.styled.jsx
│ ├── ExercisesBtnBack
│ │ ├── ExercisesBtnBack.jsx
│ │ └── ExercisesBtnBack.styled.jsx
│ ├── DailyStatsCards
│ │ ├── DailyStatsCards.jsx
│ │ └── DailyStatsCards.styled.jsx
│ ├── Modal
│ │ ├── Modal.jsx
│ │ └── Modal.styled.jsx
│ ├── DayDiary
│ │ ├── DayDiary.jsx
│ │ └── DayDiary.styled.jsx
│ ├── ExercisesItem
│ │ ├── ExercisesItem.jsx
│ │ └── ExercisesItem.styled.js
│ ├── ExercisesCategories
│ │ ├── ExercisesCategories.styled.jsx
│ │ └── ExercisesCategories.jsx
│ ├── DayDiaryProductsOrExercises
│ │ ├── DayDiaryProductsOrExercises.styled.jsx
│ │ └── DayDiaryProductsOrExercises.jsx
│ ├── AddProductForm
│ │ └── AddProductForm.jsx
│ ├── ProductOrExerciseModal
│ │ ├── ProductOrExerciseModal.styled.jsx
│ │ └── ProductOrExerciseModal.jsx
│ ├── TableForDiaryOnMobile
│ │ └── TableForDiaryOnMobile.styled.jsx
│ └── MobMenu
│ │ ├── MobMenu.styled.js
│ │ └── MobMenu.jsx
├── pages
│ ├── Params
│ │ ├── index.js
│ │ └── Params.jsx
│ ├── Profile
│ │ ├── index.js
│ │ ├── Profile.styled.js
│ │ └── Profile.jsx
│ ├── SignIn
│ │ ├── SignIn.styled.jsx
│ │ └── SignIn.jsx
│ ├── SignUp
│ │ ├── SignUp.styled.jsx
│ │ └── SignUp.jsx
│ ├── Products
│ │ ├── Products.styled.jsx
│ │ └── Products.jsx
│ ├── Exercises
│ │ └── Exercises.styled.jsx
│ ├── Error
│ │ ├── Error.jsx
│ │ └── Error.styled.jsx
│ ├── Home
│ │ ├── Home.jsx
│ │ └── Home.styled.jsx
│ └── Diary
│ │ └── Diary.styled.jsx
├── assets
│ ├── chevron-down.png
│ ├── images
│ │ ├── foodIcon.png
│ │ ├── readme.jpg
│ │ ├── thumbUp.png
│ │ ├── favicon
│ │ │ ├── favicon.png
│ │ │ └── favicon.svg
│ │ ├── exercises_desk_2x.jpeg
│ │ ├── home-page_desktop_1x.jpg
│ │ ├── home-page_desktop_2x.jpg
│ │ ├── home-page_desktop_3x.jpg
│ │ ├── home-page_mobile_1x.jpg
│ │ ├── home-page_mobile_2x.jpg
│ │ ├── home-page_mobile_3x.jpg
│ │ ├── home-page_tablet_1x.jpg
│ │ ├── home-page_tablet_2x.jpg
│ │ ├── home-page_tablet_3x.jpg
│ │ ├── products_desktop_1x.jpg
│ │ ├── products_desktop_2x.jpg
│ │ ├── products_desktop_3x.jpg
│ │ ├── params-step1_desktop_1x.jpg
│ │ ├── params-step1_desktop_2x.jpg
│ │ ├── params-step1_desktop_3x.jpg
│ │ ├── params-step1_mobile_1x.jpg
│ │ ├── params-step1_mobile_2x.jpg
│ │ ├── params-step1_mobile_3x.jpg
│ │ ├── params-step1_tablet_1x.jpg
│ │ ├── params-step1_tablet_2x.jpg
│ │ ├── params-step1_tablet_3x.jpg
│ │ ├── params-step2_desktop_1x.jpg
│ │ ├── params-step2_desktop_2x.jpg
│ │ ├── params-step2_desktop_3x.jpg
│ │ ├── params-step2_mobile_1x.jpg
│ │ ├── params-step2_mobile_2x.jpg
│ │ ├── params-step2_mobile_3x.jpg
│ │ ├── params-step2_tablet_1x.jpg
│ │ ├── params-step2_tablet_2x.jpg
│ │ ├── params-step2_tablet_3x.jpg
│ │ ├── params-step3_desktop_1x.jpg
│ │ ├── params-step3_desktop_2x.jpg
│ │ ├── params-step3_desktop_3x.jpg
│ │ ├── params-step3_mobile_1x.jpg
│ │ ├── params-step3_mobile_2x.jpg
│ │ ├── params-step3_mobile_3x.jpg
│ │ ├── params-step3_tablet_1x.jpg
│ │ ├── params-step3_tablet_2x.jpg
│ │ ├── params-step3_tablet_3x.jpg
│ │ ├── index.js
│ │ ├── imgProduct.js
│ │ ├── imgHomePage.js
│ │ └── imgParamsForm.js
│ ├── fonts
│ │ ├── roboto-black-webfont.woff
│ │ ├── roboto-bold-webfont.woff
│ │ ├── roboto-bold-webfont.woff2
│ │ ├── roboto-light-webfont.woff
│ │ ├── roboto-black-webfont.woff2
│ │ ├── roboto-light-webfont.woff2
│ │ ├── roboto-medium-webfont.woff
│ │ ├── roboto-medium-webfont.woff2
│ │ ├── roboto-regular-webfont.woff
│ │ └── roboto-regular-webfont.woff2
│ └── shewron.svg
├── utils
│ ├── capitalizeWord.js
│ ├── customBtn.js
│ ├── handleLogout.js
│ ├── titleMargins.js
│ ├── svgUser.js
│ ├── titleMarginForDairyPage.js
│ ├── isTheSameForm.js
│ ├── mediaQuery.js
│ ├── formatNumberStatistics.js
│ ├── textLength.js
│ ├── getCurrentDate.js
│ ├── descriptionTextMargin.js
│ ├── index.js
│ ├── formatDate.js
│ ├── colorVeriables.js
│ └── pageContentToRender.js
├── redux
│ ├── products
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ ├── exerciseFilters
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ ├── productsFilter
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ ├── exercises
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ ├── auth
│ │ ├── selectors.js
│ │ └── operation.js
│ ├── statistic
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ ├── diary
│ │ ├── selectors.js
│ │ ├── operations.js
│ │ └── slice.js
│ └── store.js
├── data
│ └── productsCategories.json
├── hooks
│ ├── useAuth.js
│ └── useMatchMedia.js
├── main.jsx
├── index.css
└── App.jsx
├── .huskyrc
├── .husky
└── pre-commit
├── .lintstagedrc
├── .prettierrc.json
├── vite.config.js
├── .gitignore
├── index.html
├── .eslintrc.cjs
├── .github
└── workflows
│ └── deploy.yml
└── package.json
/.env.template:
--------------------------------------------------------------------------------
1 | VITE_API_TEST=Hello,world
--------------------------------------------------------------------------------
/src/components/Timer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Timer';
2 |
--------------------------------------------------------------------------------
/src/pages/Params/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Params';
2 |
--------------------------------------------------------------------------------
/src/pages/Profile/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Profile';
2 |
--------------------------------------------------------------------------------
/src/components/AuthForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './AuthForm';
2 |
--------------------------------------------------------------------------------
/src/components/BtnSubmit/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './BtnSubmit';
2 |
--------------------------------------------------------------------------------
/src/components/Calendar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Calendar';
2 |
--------------------------------------------------------------------------------
/src/components/Checkbox/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Checkbox';
2 |
--------------------------------------------------------------------------------
/src/components/ParamsBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ParamsBar';
2 |
--------------------------------------------------------------------------------
/src/components/ParamsBtn/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ParamsBtn';
2 |
--------------------------------------------------------------------------------
/src/components/ParamsForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ParamsForm';
2 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Scrollbar';
2 |
--------------------------------------------------------------------------------
/src/components/UserCard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UserCard';
2 |
--------------------------------------------------------------------------------
/src/components/UserForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UserForm';
2 |
--------------------------------------------------------------------------------
/src/pages/SignIn/SignIn.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged"
4 | }
5 | }
--------------------------------------------------------------------------------
/src/components/AddExerciseForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './AddExerciseForm';
2 |
--------------------------------------------------------------------------------
/src/components/ParamsBlockСard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ParamsBlockCard';
2 |
--------------------------------------------------------------------------------
/src/components/ProductsFilter/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ProductsFilter';
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/src/pages/SignUp/SignUp.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 |
4 |
5 | `;
--------------------------------------------------------------------------------
/src/components/ButtonIconForInput/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ButtonIconForInput';
2 |
--------------------------------------------------------------------------------
/src/components/ProductOrExerciseContainer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ProductOrExerciseContainer';
2 |
--------------------------------------------------------------------------------
/src/assets/chevron-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/chevron-down.png
--------------------------------------------------------------------------------
/src/assets/images/foodIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/foodIcon.png
--------------------------------------------------------------------------------
/src/assets/images/readme.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/readme.jpg
--------------------------------------------------------------------------------
/src/assets/images/thumbUp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/thumbUp.png
--------------------------------------------------------------------------------
/src/assets/images/favicon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/favicon/favicon.png
--------------------------------------------------------------------------------
/src/assets/images/exercises_desk_2x.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/exercises_desk_2x.jpeg
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-black-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-black-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-bold-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-bold-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-light-webfont.woff
--------------------------------------------------------------------------------
/src/assets/images/home-page_desktop_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_desktop_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_desktop_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_desktop_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_desktop_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_desktop_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_mobile_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_mobile_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_mobile_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_mobile_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_mobile_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_mobile_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_tablet_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_tablet_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_tablet_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_tablet_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/home-page_tablet_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/home-page_tablet_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/products_desktop_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/products_desktop_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/products_desktop_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/products_desktop_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/products_desktop_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/products_desktop_3x.jpg
--------------------------------------------------------------------------------
/src/utils/capitalizeWord.js:
--------------------------------------------------------------------------------
1 | export function capitalizeWord(word) {
2 | return word.substring(0, 1).toUpperCase() + word.substring(1);
3 | }
4 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "src/**/*.{json,css,scss,md}": ["prettier --write"],
3 | "src/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"]
4 | }
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-black-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-black-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-light-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-light-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-medium-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-medium-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-medium-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-regular-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/roboto-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/fonts/roboto-regular-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/images/params-step1_desktop_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_desktop_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_desktop_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_desktop_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_desktop_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_desktop_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_mobile_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_mobile_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_mobile_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_mobile_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_mobile_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_mobile_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_tablet_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_tablet_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_tablet_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_tablet_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step1_tablet_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step1_tablet_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_desktop_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_desktop_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_desktop_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_desktop_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_desktop_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_desktop_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_mobile_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_mobile_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_mobile_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_mobile_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_mobile_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_mobile_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_tablet_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_tablet_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_tablet_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_tablet_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step2_tablet_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step2_tablet_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_desktop_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_desktop_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_desktop_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_desktop_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_desktop_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_desktop_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_mobile_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_mobile_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_mobile_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_mobile_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_mobile_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_mobile_3x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_tablet_1x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_tablet_1x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_tablet_2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_tablet_2x.jpg
--------------------------------------------------------------------------------
/src/assets/images/params-step3_tablet_3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MargoMarm/september-project/HEAD/src/assets/images/params-step3_tablet_3x.jpg
--------------------------------------------------------------------------------
/src/components/Routes/index.js:
--------------------------------------------------------------------------------
1 | export { default as PrivateRoute } from './PrivateRoute';
2 | export { default as PublicRoute } from './PublicRoute';
3 |
--------------------------------------------------------------------------------
/src/redux/products/selectors.js:
--------------------------------------------------------------------------------
1 | export const getAddProductIsLoading = state => state.products.isLoading;
2 |
3 | export const getAddProductError = state => state.products.error;
4 |
--------------------------------------------------------------------------------
/src/utils/customBtn.js:
--------------------------------------------------------------------------------
1 | const button = {
2 | margin: '0',
3 | padding: '0',
4 | border: 'none',
5 | backgroundColor: ' rgba(0, 0, 0, 0)',
6 | };
7 |
8 | export default button;
--------------------------------------------------------------------------------
/src/utils/handleLogout.js:
--------------------------------------------------------------------------------
1 | import { logOutUser } from '../redux/auth/operation';
2 |
3 | const handleLogout = dispatch => {
4 | dispatch(logOutUser());
5 | };
6 |
7 | export default handleLogout;
8 |
--------------------------------------------------------------------------------
/src/utils/titleMargins.js:
--------------------------------------------------------------------------------
1 | const mg = {
2 | top: {
3 | desk: 116,
4 | tab: 105,
5 | mob: 66,
6 | },
7 | bt: {
8 | tab: 16,
9 | mob: 14,
10 | },
11 | };
12 |
13 | export default mg;
14 |
--------------------------------------------------------------------------------
/src/assets/images/index.js:
--------------------------------------------------------------------------------
1 | export { default as imgForHome } from './imgHomePage';
2 |
3 | export { default as imgProducts } from './imgProduct';
4 |
5 | export { default as imgPrmsForm } from './imgParamsForm';
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/utils/svgUser.js:
--------------------------------------------------------------------------------
1 | export const svgUser = {
2 | width: '46px',
3 | height: '46px',
4 | fill: 'rgba(48, 48, 48, 0.3)',
5 | strokeWidth: '1px',
6 | stroke: ' #e6533c',
7 | };
8 |
9 | export default svgUser;
10 |
--------------------------------------------------------------------------------
/src/utils/titleMarginForDairyPage.js:
--------------------------------------------------------------------------------
1 | export const mgForTitle = {
2 | top: {
3 | desk: 72,
4 | tab: 72,
5 | mob: 40,
6 | },
7 | bt: {
8 | tab: 32,
9 | mob: 40,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/assets/shewron.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/isTheSameForm.js:
--------------------------------------------------------------------------------
1 | export default function isTheSameForm(iv, fv) {
2 | const arr = Object.keys(iv);
3 | let bool = true;
4 | arr.forEach(item => {
5 | if (iv[item] != fv[item]) bool = false;
6 | });
7 |
8 | return bool;
9 | }
10 |
--------------------------------------------------------------------------------
/src/redux/exerciseFilters/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectItems = state => state.filter.items;
2 | export const selectFilter = state => state.filter.filter;
3 | export const selectCurrentTitle = state => state.filter.currentTitle;
4 | export const selectIsLoading = state => state.filter.isLoading;
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "avoid",
11 | "proseWrap": "always"
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/mediaQuery.js:
--------------------------------------------------------------------------------
1 | const breakpoints = [320, 375, 768, 1440];
2 |
3 | const [smallMobile, mobile, tablet, desktop] = breakpoints.map(
4 | bp => `@media (min-width: ${bp}px)`,
5 | );
6 |
7 | const mq = {
8 | smallMobile,
9 | mobile,
10 | tablet,
11 | desktop,
12 | };
13 |
14 | export default mq;
15 |
--------------------------------------------------------------------------------
/src/assets/images/imgProduct.js:
--------------------------------------------------------------------------------
1 | import imgDx1 from '../images/products_desktop_1x.jpg';
2 | import imgDx2 from '../images/products_desktop_2x.jpg';
3 | import imgDx3 from '../images/products_desktop_3x.jpg';
4 |
5 | const imgProducts = {
6 | imgDx1,
7 | imgDx2,
8 | imgDx3
9 | }
10 |
11 | export default imgProducts
--------------------------------------------------------------------------------
/src/components/SubTitle/SubTitle.jsx:
--------------------------------------------------------------------------------
1 | import { SubStyle } from './SubTitle.styled';
2 | import PropTypes from 'prop-types';
3 |
4 | const SubTitle = ({ text }) => {
5 | return {text};
6 | };
7 |
8 | SubTitle.propTypes = {
9 | text: PropTypes.string,
10 | };
11 |
12 | export default SubTitle;
13 |
--------------------------------------------------------------------------------
/src/utils/formatNumberStatistics.js:
--------------------------------------------------------------------------------
1 | function formatNumber(number) {
2 | if (number >= 1e6) {
3 | return (number / 1e6).toFixed(1) + 'M';
4 | } else if (number >= 1e3) {
5 | return (number / 1e3).toFixed(1) + 'K';
6 | } else {
7 | return number.toString();
8 | }
9 | }
10 |
11 | export default formatNumber;
12 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 |
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | src: "/src",
10 | components: "/src/components",
11 | },
12 | },
13 | base: "/september-project/",
14 | });
--------------------------------------------------------------------------------
/src/components/Scrollbar/Scrollbar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import { ScrollContainer } from './Scrollbar.syled';
4 |
5 | export default function ScrollBar({ width, children }) {
6 | return {children};
7 | }
8 |
9 | ScrollBar.propTypes = {
10 | width: PropTypes.object,
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/textLength.js:
--------------------------------------------------------------------------------
1 | const textLength = (name, windowWidth) => {
2 | let maxLength = 24;
3 |
4 | if (windowWidth < 768) {
5 | maxLength = 23;
6 | } else if (windowWidth < 1440) {
7 | maxLength = 19;
8 | }
9 |
10 | return name.length <= maxLength ? name : name.substring(0, maxLength) + '...';
11 | };
12 |
13 | export default textLength;
14 |
--------------------------------------------------------------------------------
/src/components/LoaderForPages/LoaderForPages.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const LoaderContainer = styled.div`
4 | position: fixed;
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | width: 100%;
9 | height: 100%;
10 | z-index: 200;
11 |
12 | background-color: rgba(0, 0, 0, 0.5);
13 | `;
14 |
--------------------------------------------------------------------------------
/src/redux/productsFilter/selectors.js:
--------------------------------------------------------------------------------
1 | export const getProducts = state => state.products.products;
2 | export const getProductsCategories = state => state.products.categories;
3 | export const getIsLoading = state => state.products.isLoading;
4 | export const getSearchParams = state => state.products.searchParams;
5 | export const getHasMore = state => state.products.hasMore;
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 |
--------------------------------------------------------------------------------
/src/components/Title/Title.jsx:
--------------------------------------------------------------------------------
1 | import { StyledTitle } from './Title.styled';
2 | import PropTypes from 'prop-types';
3 | import 'animate.css';
4 |
5 | const Title = ({ text, margin }) => {
6 | return {text};
7 | };
8 |
9 | Title.propTypes = {
10 | text: PropTypes.string,
11 | margin: PropTypes.object,
12 | };
13 |
14 | export default Title;
15 |
--------------------------------------------------------------------------------
/src/components/ExercisesItemList/ExercisesItemList.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils/index';
3 |
4 | export const ExercisesItemList = styled.ul`
5 | margin-top: 40px;
6 |
7 | padding-bottom: 52px;
8 |
9 | ${mq.tablet} {
10 | display: flex;
11 | flex-direction: row;
12 | flex-wrap: wrap;
13 | gap: 16px;
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/src/utils/getCurrentDate.js:
--------------------------------------------------------------------------------
1 | export const getCurrentDate = () => {
2 | const dateNow = new Date();
3 | const day = dateNow.getUTCDate();
4 | const month = dateNow.getUTCMonth() + 1;
5 | const year = dateNow.getUTCFullYear();
6 | const date = `${day.toString().length > 1 ? day : '0' + day}-${
7 | month.toString().length > 1 ? month : '0' + month
8 | }-${year}`;
9 | return date;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/SharedLayout/SheradLayout.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { mq } from '../../utils';
4 |
5 | export const Container = styled.div`
6 | position: relative;
7 | margin: 0px auto;
8 |
9 | ${mq.mobile} {
10 | width: 375px;
11 | }
12 |
13 | ${mq.tablet} {
14 | width: 768px;
15 | }
16 |
17 | ${mq.desktop} {
18 | width: 1440px;
19 | }
20 | `;
21 |
--------------------------------------------------------------------------------
/src/data/productsCategories.json:
--------------------------------------------------------------------------------
1 | [
2 | "alcoholic drinks",
3 | "berries",
4 | "cereals",
5 | "dairy",
6 | "dried fruits",
7 | "eggs",
8 | "fish",
9 | "flour",
10 | "fruits",
11 | "meat",
12 | "mushrooms",
13 | "nuts",
14 | "oils and fats",
15 | "poppy",
16 | "sausage",
17 | "seeds",
18 | "sesame",
19 | "soft drinks",
20 | "vegetables and herbs"
21 | ]
--------------------------------------------------------------------------------
/src/components/SubTitle/SubTitle.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const SubStyle = styled.p`
5 | color: ${colors.textWhite03};
6 |
7 | font-family: Roboto;
8 | font-size: 14px;
9 | font-style: normal;
10 | line-height: 1.28;
11 |
12 |
13 | ${mq.tablet} {
14 | max-width: 496px;
15 | font-size: 16px;
16 | line-height: 1.5;
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/MainTitle/MainTitle.jsx:
--------------------------------------------------------------------------------
1 | import { Text } from './MainTitle.styled';
2 | import sprite from '../../assets/sprite.svg';
3 |
4 | export default function MainTitle() {
5 | return (
6 |
7 | Transforming your{' '}
8 |
9 | body{' '}
10 |
13 | {' '}
14 | shape with Power Pulse
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/descriptionTextMargin.js:
--------------------------------------------------------------------------------
1 | export const mgForDiary = {
2 | top: {
3 | mobile: 20,
4 | tablet: 32,
5 | desktop: 48,
6 | },
7 | bottom: {
8 | mobile: 40,
9 | tablet: 0,
10 | desktop: 0,
11 | },
12 | };
13 |
14 | export const mgForProducts = {
15 | top: {
16 | mobile: 40,
17 | tablet: 32,
18 | desktop: 32,
19 | },
20 | bottom: {
21 | mobile: 41,
22 | tablet: 32,
23 | desktop: 32,
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/redux/exercises/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectItems = state => state.exercises.items;
2 |
3 | export const selectGetFilters = state => state.exercises.getFilters;
4 |
5 | export const selectIsLoadingExercises = state => state.exercises.isLoading;
6 |
7 | export const selectHasMore = state => state.exercises.hasMore;
8 |
9 | export const selectSearchParams = state => state.exercises.searchParams;
10 |
11 | export const isTimerOn = state => state.exercises.isTimerOn;
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Power Pulse
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as colors } from './colorVeriables';
2 | export { default as mq } from './mediaQuery';
3 | export { default as button } from './customBtn';
4 | export { default as svgUser } from './svgUser';
5 | export { default as pageContentToRender } from './pageContentToRender';
6 | export { default as mg } from './titleMargins';
7 | export { default as handleLogout } from './handleLogout';
8 | export { default as isTheSameForm } from './isTheSameForm';
9 |
10 |
--------------------------------------------------------------------------------
/src/components/LinkSubText/LinkSubText.jsx:
--------------------------------------------------------------------------------
1 | import { Text, Link } from './LinkSubText.styled';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | const LinkSubText = ({ to, linkText, text }) => {
6 | return (
7 |
8 | {text}
9 | {linkText}
10 |
11 | );
12 | };
13 |
14 | LinkSubText.propTypes = {
15 | to: PropTypes.string,
16 | linkText: PropTypes.string,
17 | text: PropTypes.string,
18 | };
19 |
20 | export default LinkSubText;
21 |
--------------------------------------------------------------------------------
/src/utils/formatDate.js:
--------------------------------------------------------------------------------
1 | const formatDate = date => {
2 | const dateObject = date;
3 | let day = dateObject.getDate();
4 | let month = dateObject.getMonth() + 1; // Months are 0-based, so add 1
5 | const year = dateObject.getFullYear();
6 |
7 | if (day < 10) {
8 | day = '0' + day;
9 | }
10 | if (month < 10) {
11 | month = '0' + month;
12 | }
13 |
14 | const formatted_date = day + '-' + month + '-' + year;
15 | return formatted_date;
16 | };
17 |
18 | export default formatDate;
--------------------------------------------------------------------------------
/src/components/Routes/PublicRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import PropTypes from 'prop-types';
3 | import { UseAuth } from '../../hooks/useAuth';
4 |
5 | const PublicRoute = ({ component, redirectTo }) => {
6 | const { isLoggedIn } = UseAuth();
7 |
8 | return isLoggedIn ? : component;
9 | };
10 |
11 | PublicRoute.propTypes = {
12 | component: PropTypes.object.isRequired,
13 | redirectTo: PropTypes.string,
14 | };
15 |
16 | export default PublicRoute;
17 |
--------------------------------------------------------------------------------
/src/components/DiaryStatisticsList/DiaryStatisticsList.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const List = styled.ul`
5 | display: grid;
6 | gap: 13px;
7 |
8 | grid-template-columns: repeat(2, 1fr);
9 |
10 | ${mq.tablet} {
11 | grid-template-columns: repeat(3, 1fr);
12 | gap: 16px;
13 | width: 593px;
14 |
15 |
16 | }
17 | ${mq.desktop} {
18 | grid-template-columns: repeat(2, 1fr);
19 | width: 390px;
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/components/LinkSubText/LinkSubText.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import { colors } from '../../utils';
5 | import { color } from '@mui/system';
6 |
7 | export const Text = styled.p`
8 | margin-top: 12px;
9 | color: ${color.textWhite06};
10 | font-size: 12px;
11 | line-height: 1.5;
12 | `;
13 |
14 | export const Link = styled(NavLink)`
15 | margin-left: 5px;
16 | color: ${colors.white};
17 | text-decoration-line: underline;
18 | `;
19 |
--------------------------------------------------------------------------------
/src/redux/auth/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectIsLoggedIn = state => state.auth.isLoggedIn;
2 |
3 | export const selectIsRefreshing = state => state.auth.isRefreshing;
4 |
5 | export const selectUser = state => state.auth.user;
6 |
7 | export const selectDailyTime = state => state.auth.user.dailyTime;
8 |
9 | export const selectDailyСalories = state => state.auth.user.dailyСalories;
10 |
11 | export const selectError = state => state.auth.error;
12 |
13 | export const isLoadingSignInOrSignUp = state => state.auth.isLoading;
14 |
--------------------------------------------------------------------------------
/src/components/ParamsBar/ParamsBar.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils';
3 |
4 | export const ProgressBar = styled.div`
5 | display: flex;
6 | justify-content: space-around;
7 | width: 178px;
8 | height: 4px;
9 |
10 | ${mq.tablet} {
11 | width: 268px;
12 | }
13 | `;
14 |
15 | export const BarItem = styled.div`
16 | width: 50px;
17 | height: 4px;
18 | border-radius: 2px;
19 |
20 | background: #303030;
21 |
22 | ${mq.tablet} {
23 | width: 80px;
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/components/headersComp/Logo/Logo.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, button } from '../../../utils';
3 |
4 | export const Button = styled.button`
5 | ${button}
6 | `;
7 |
8 | export const WrapLogo = styled.div`
9 | display: flex;
10 | align-items: flex-end;
11 | gap: 5px;
12 | `;
13 |
14 | export const Svg = styled.svg`
15 | animation: pulse 2s ease-in-out infinite alternate;
16 | width: 126px;
17 | height: 13px;
18 | ${mq.tablet} {
19 | width: 152px;
20 | height: 17px;
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/src/components/CustomNavLink/CustomNavLink.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from './CustomNavLink.styled';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | const CustomNavLink = ({ to, isorange, isinheader, text }) => {
6 | return (
7 |
8 | {text}
9 |
10 | );
11 | };
12 |
13 | CustomNavLink.propTypes = {
14 | to: PropTypes.string,
15 | isorange: PropTypes.string,
16 | isinheader: PropTypes.string,
17 | text: PropTypes.string,
18 | };
19 |
20 | export default CustomNavLink;
21 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import {
3 | selectError,
4 | selectIsLoggedIn,
5 | selectIsRefreshing,
6 | selectUser,
7 | } from '../redux/auth/selectors';
8 |
9 | export const UseAuth = () => {
10 | const user = useSelector(selectUser);
11 | const isLoggedIn = useSelector(selectIsLoggedIn);
12 | const isRefreshing = useSelector(selectIsRefreshing);
13 | const error = useSelector(selectError);
14 |
15 | return {
16 | user,
17 | isLoggedIn,
18 | isRefreshing,
19 | error,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/ButtonIconForInput/ButtonIconForInput.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { Button } from './ButtonIconForInput.styled';
3 |
4 | export default function ButtonIconForInput({ children, onClick, right, type, }) {
5 | return (
6 |
9 | );
10 | }
11 |
12 | ButtonIconForInput.propTypes = {
13 | // onClick: PropTypes.func.isRequired,
14 | right: PropTypes.string,
15 | type: PropTypes.string,
16 | children: PropTypes.object.isRequired,
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/UserForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | export default yup.object({
4 | name: yup.string().required(),
5 | height: yup.number().min(150).max(250).required(),
6 | currentWeight: yup.number().min(35).max(200).required(),
7 | desiredWeight: yup.number().min(35).max(200).required(),
8 | birthday: yup.date().required(),
9 | blood: yup.string().oneOf(['1', '2', '3', '4']).required(),
10 | sex: yup.string().oneOf(['male', 'female']).required(),
11 | levelActivity: yup.string().oneOf(['1', '2', '3', '4', '5']).required(),
12 | });
13 |
--------------------------------------------------------------------------------
/src/redux/statistic/selectors.js:
--------------------------------------------------------------------------------
1 | export const getAllExercises = state => state.statistics.allExercises;
2 |
3 | export const getAllUsers = state => state.statistics.allUsers;
4 |
5 | export const getUsersBurnedCalories = state => state.statistics.usersBurnedCalories;
6 |
7 | export const getUsersTimeTraining = state => state.statistics.usersTimeTraining;
8 |
9 | export const getUsersTraining = state => state.statistics.usersTraining;
10 |
11 | export const isLoadingStatictics = state => state.statistics.isLoading;
12 |
13 | export const getErrorStatistics = state => state.statistics.error;
14 |
--------------------------------------------------------------------------------
/src/components/Routes/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import PropTypes from 'prop-types';
3 | import { UseAuth } from '../../hooks/useAuth';
4 |
5 | const PrivateRoute = ({ component, redirectTo = '/' }) => {
6 | const { isLoggedIn, isRefreshing } = UseAuth();
7 | const shouldRedirect = isLoggedIn && !isRefreshing;
8 |
9 | return shouldRedirect ? component : ;
10 | };
11 |
12 | PrivateRoute.propTypes = {
13 | component: PropTypes.object.isRequired,
14 | redirectTo: PropTypes.string,
15 | };
16 |
17 | export default PrivateRoute;
18 |
--------------------------------------------------------------------------------
/src/components/ProductOrExerciseContainer/ProductOrExerciseContainer.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils';
3 |
4 | export const Container = styled.ul`
5 | display: flex;
6 | flex-direction: column;
7 | align-items: flex-start;
8 | justify-content: flex-start;
9 | align-content: flex-start;
10 | gap: 20px;
11 | width: 100%;
12 |
13 | margin-top: ${props => props.marginTop};
14 | ${mq.tablet} {
15 | flex-direction: row;
16 | flex-wrap: wrap;
17 | row-gap: 32px;
18 | column-gap: 16px;
19 | }
20 | ${mq.desktop} {
21 | width: 868px;
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | 'prettier'
10 | ],
11 | ignorePatterns: ['dist', '.eslintrc.cjs'],
12 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
13 | settings: { react: { version: '18.2' } },
14 | plugins: ['react-refresh'],
15 | rules: {
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Lodaer/Loader.jsx:
--------------------------------------------------------------------------------
1 | import { Puff } from 'react-loader-spinner';
2 |
3 | const Loader = ({ size, needToCenter }) => {
4 | return (
5 |
21 | );
22 | };
23 |
24 | export default Loader;
25 |
--------------------------------------------------------------------------------
/src/redux/products/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { toast } from 'react-toastify';
4 |
5 | axios.defaults.baseURL = 'https://power-pulse-rest-api.onrender.com';
6 |
7 | export const addProduct = createAsyncThunk(
8 | 'addProduct',
9 | async (productDetails, { rejectWithValue }) => {
10 | try {
11 | await axios.post('/api/diary/add-product', productDetails);
12 | } catch (error) {
13 | toast.error('Oops... Something went wrong! Try again!');
14 | return rejectWithValue('Oops... Something went wrong!');
15 | }
16 | },
17 | );
18 |
--------------------------------------------------------------------------------
/src/components/BtnSubmit/BtnSubmit.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { ButtonSubmit } from './BtnSubmit.styled';
3 |
4 | export default function BtnSubmit({
5 | title,
6 | margin,
7 | fontSize,
8 | btnNext = () => null,
9 | }) {
10 | return (
11 |
17 | {title}
18 |
19 | );
20 | }
21 |
22 | BtnSubmit.propTypes = {
23 | title: PropTypes.string.isRequired,
24 | margin: PropTypes.object,
25 | fontSize: PropTypes.string,
26 | btnNext: PropTypes.func,
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/BtnSubtitle/BtnSubtitle.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import {
4 | BtnSubtitleText,
5 | BtnSubtitleLink,
6 | TextWrapper,
7 | } from './BtnSubtitle.styled';
8 |
9 | const BtnSubtitle = ({ text, linkText, to }) => {
10 | return (
11 |
12 | {text}
13 | {linkText}
14 |
15 | );
16 | };
17 |
18 | BtnSubtitle.propTypes = {
19 | text: PropTypes.string.isRequired,
20 | linkText: PropTypes.string.isRequired,
21 | to: PropTypes.string.isRequired,
22 | };
23 |
24 | export default BtnSubtitle;
25 |
--------------------------------------------------------------------------------
/src/components/headersComp/Logo/Logo.jsx:
--------------------------------------------------------------------------------
1 | import { WrapLogo, Svg } from './Logo.styled';
2 | import sprite from '../../../assets/sprite.svg';
3 | import 'animate.css';
4 | import { UseAuth } from '../../../hooks/useAuth';
5 | import { NavLink } from 'react-router-dom';
6 |
7 | export const Logo = () => {
8 | const { isLoggedIn } = UseAuth();
9 | return (
10 | <>
11 |
12 |
13 | {' '}
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default Logo;
23 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | env:
7 | VITE_API_TEST: ${{secrets.VITE_API_TEST}}
8 |
9 | jobs:
10 | build-and-deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout 🛎️
14 | uses: actions/checkout@v3
15 |
16 | - name: Install, build 🔧
17 | run: |
18 | npm install
19 | npm run build
20 | cp ./dist/index.html ./dist/404.html
21 |
22 | - name: Deploy 🚀
23 | uses: JamesIves/github-pages-deploy-action@4.1.0
24 | with:
25 | branch: gh-pages
26 | folder: dist
--------------------------------------------------------------------------------
/src/components/CustomInputForCalendar/CustomInputForCalendar.jsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import { Icon, Input, Label } from './CustomInputForCalendar.styled';
3 |
4 | import sprite from '../../assets/sprite.svg';
5 |
6 | const CustomInputForCalendar = forwardRef((dd, ref) => {
7 | const { value, onClick } = dd;
8 | return (
9 |
15 | );
16 | });
17 |
18 | CustomInputForCalendar.displayName = 'CustomInputForCalendar';
19 |
20 | export default CustomInputForCalendar;
21 |
--------------------------------------------------------------------------------
/src/components/EmptyProductList/EmptyProductList.jsx:
--------------------------------------------------------------------------------
1 | import {TextContainer, MainText, AccentText } from './EmptyProductList.styled';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | const EmptyProductList = () => {
6 | return (
7 |
8 | Sorry, no results were found for the product filters you selected. You may want to consider other search options to find the product you want. Our range is wide and you have the opportunity to find more options that suit your needs.
9 | Try changing the search parameters.
10 |
11 | );
12 | };
13 |
14 |
15 |
16 | export default EmptyProductList;
--------------------------------------------------------------------------------
/src/components/LoaderForPages/LoaderForPages.jsx:
--------------------------------------------------------------------------------
1 | import { LoaderContainer } from './LoaderForPages.styled';
2 |
3 | import { Puff } from 'react-loader-spinner';
4 |
5 | const LoaderForPages = () => {
6 | return (
7 |
8 |
22 |
23 | );
24 | };
25 |
26 | export default LoaderForPages;
27 |
--------------------------------------------------------------------------------
/src/components/DescriptionText/DescriptionText.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { ExclamationIcon, ExclamationText } from './DescriptionText.styled';
3 | import sprite from '../../assets/sprite.svg';
4 |
5 | const DescriptionText = ({ text, width, margin }) => {
6 | return (
7 |
8 |
9 |
10 |
11 | {text}
12 |
13 | );
14 | };
15 |
16 | DescriptionText.propTypes = {
17 | text: PropTypes.string,
18 | width: PropTypes.object,
19 | margin: PropTypes.object,
20 | };
21 |
22 | export default DescriptionText;
23 |
--------------------------------------------------------------------------------
/src/components/headersComp/Header/Header.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../../utils';
3 |
4 | export const HeaderContainer = styled.header`
5 | position: relative;
6 | background-color: ${colors.black};
7 | height: 61px;
8 | display: flex;
9 | justify-content: space-between;
10 | align-items: center;
11 | padding: 0 20px;
12 | border-bottom: ${props =>
13 | props.isBorderToRender ? `1px solid ${colors.textWhite03}` : 'none'};
14 | ${mq.tablet} {
15 | background-color: ${colors.black};
16 | padding: 0 32px;
17 | height: 84px;
18 | }
19 |
20 | ${mq.desktop} {
21 | background-color: transparent;
22 | padding-left: 96px;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/src/components/ButtonIconForInput/ButtonIconForInput.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '../../utils';
3 |
4 | export const Button = styled.button`
5 | position: absolute;
6 | top: 50%;
7 | right: ${({ right }) => right || '0'};
8 | transform: translateY(-50%);
9 | padding: 8px;
10 | border: none;
11 | outline: none;
12 | display: inline-flex;
13 | align-items: center;
14 | justify-content: center;
15 | background-color: transparent;
16 | color: ${colors.white};
17 | transition:
18 | scale 200ms cubic-bezier(0.4, 0, 0.2, 1),
19 | color 200ms cubic-bezier(0.4, 0, 0.2, 1);
20 |
21 | &:hover {
22 | color: ${colors.orange};
23 | scale: 1.15;
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/components/ProductOrExerciseContainer/ProductOrExerciseContainer.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 |
4 | import { getSearchParams } from '../../redux/productsFilter/selectors';
5 | import { Container } from './ProductOrExerciseContainer.styled';
6 |
7 | export default function ProductsOrExercisesContainer({ children, ...props }) {
8 | const containerRef = useRef();
9 | const searchParams = useSelector(getSearchParams);
10 |
11 | useEffect(() => {
12 | containerRef.current.firstChild?.scrollIntoView();
13 | }, [searchParams]);
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import ReactDOM from 'react-dom/client';
4 | import App from './App.jsx';
5 | import './index.css';
6 | import { PersistGate } from 'redux-persist/integration/react';
7 | import { Provider } from 'react-redux';
8 |
9 | import { persistor, store } from './redux/store.js';
10 |
11 | ReactDOM.createRoot(document.getElementById('root')).render(
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | );
22 |
--------------------------------------------------------------------------------
/src/redux/exerciseFilters/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 |
4 |
5 |
6 | export const fetchFilters = createAsyncThunk(
7 | 'filters/getFilters',
8 | async (_, thunkAPI) => {
9 | try {
10 | const response = await axios.get(`api/filter`);
11 | return response.data;
12 | } catch (error) {
13 | return thunkAPI.rejectWithValue(error.message);
14 | }
15 | },
16 | {
17 | condition: (_, { getState, extra }) => {
18 | const state = getState();
19 | if (state.filter.items.length > 1) {
20 | return false
21 | }
22 | }
23 | }
24 |
25 | );
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/ExercisesBtnBack/ExercisesBtnBack.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { changeStatusFilter } from '../../redux/exercises/slice';
3 | import { ButtonBack, SvgArrow } from './ExercisesBtnBack.styled'; // Імпорт стилів
4 | import sprite from '../../assets/sprite.svg';
5 |
6 | const ExercisesBtnBack = () => {
7 | const dispatch = useDispatch();
8 |
9 | const handleBtnBack = () => {
10 | dispatch(changeStatusFilter(true));
11 | };
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | Back
19 |
20 | );
21 | };
22 |
23 | export default ExercisesBtnBack;
24 |
--------------------------------------------------------------------------------
/src/components/EmptyProductList/EmptyProductList.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils';
3 |
4 | export const TextContainer = styled.div`
5 |
6 | ${mq.tablet} {
7 | width: 580px;
8 | margin-top: 45px;
9 | }
10 | ${mq.desktop} {
11 | margin-top: 100px;
12 | }
13 | `;
14 | export const MainText = styled.p`
15 | color: rgba(239, 237, 232, 0.30);
16 | margin-bottom: 16px;
17 | font-size: 14px;
18 | line-height: 1.28;
19 | ${mq.tablet} {
20 |
21 | }
22 | ${mq.desktop} {
23 |
24 | }
25 | `;
26 | export const AccentText = styled.span`
27 | color: #E6533C;
28 |
29 | font-size: 14px;
30 |
31 | line-height: 1.28;
32 | ${mq.tablet} {
33 | gap: 32px;
34 | }
35 | ${mq.desktop} {
36 |
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/assets/images/imgHomePage.js:
--------------------------------------------------------------------------------
1 | import imgDx1 from '../images/home-page_desktop_1x.jpg';
2 | import imgDx2 from '../images/home-page_desktop_2x.jpg';
3 | import imgDx3 from '../images/home-page_desktop_3x.jpg';
4 |
5 | import imgTx1 from '../images/home-page_tablet_1x.jpg';
6 | import imgTx2 from '../images/home-page_tablet_2x.jpg';
7 | import imgTx3 from '../images/home-page_tablet_3x.jpg';
8 |
9 | import imgMx1 from '../images/home-page_mobile_1x.jpg';
10 | import imgMx2 from '../images/home-page_mobile_2x.jpg';
11 | import imgMx3 from '../images/home-page_mobile_3x.jpg';
12 |
13 | const imgForHome = {
14 | imgDx1,
15 | imgDx2,
16 | imgDx3,
17 | imgTx1,
18 | imgTx2,
19 | imgTx3,
20 | imgMx1,
21 | imgMx2,
22 | imgMx3,
23 | };
24 |
25 | export default imgForHome;
26 |
--------------------------------------------------------------------------------
/src/redux/diary/selectors.js:
--------------------------------------------------------------------------------
1 | export const getDiaryProducts = state => state.diary.products;
2 |
3 | export const getDiaryExercises = state => state.diary.exercises;
4 |
5 | export const getIsLoadingDiary = state => state.diary.isLoading;
6 |
7 | export const getIsLoadingExercies = state => state.diary.isLoadingExercies;
8 |
9 | export const getIsLoadingPrfoducts = state => state.diary.isLoadingProducts;
10 |
11 | export const getError = state => state.diary.error;
12 |
13 | export const getErrorProductsAndExercisesError = state =>
14 | state.diary.productsAndExercisesError;
15 |
16 | export const burnedCalories = state => state.diary.burnedCalories;
17 |
18 | export const consumedCalories = state => state.diary.consumedCalories;
19 |
20 | export const doneExercisesTime = state => state.diary.doneExercisesTime;
21 |
--------------------------------------------------------------------------------
/src/utils/colorVeriables.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | black: '#040404',
3 | modalBlack: '#10100F',
4 | white: '#EFEDE8',
5 | orange: '#E6533C',
6 | grey: '#efede8',
7 | orangeSecondary: '#EF8964',
8 | textSuccess: '#3CBF61',
9 | textError: '#D80027',
10 | red: '#E9101D',
11 | green: '#3CBF61',
12 | backdrop: 'rgba(22, 22, 22, 0.5)',
13 | textWhite005: 'rgba(239, 237, 232, 0.05)',
14 | textWhite01: 'rgba(239, 237, 232, 0.10)',
15 | textWhite02: 'rgba(239, 237, 232, 0.20)',
16 | textWhite03: 'rgba(239, 237, 232, 0.30)',
17 | textWhite04: 'rgba(239, 237, 232, 0.40)',
18 | textWhite05: 'rgba(239, 237, 232, 0.50)',
19 | textWhite06: 'rgba(239, 237, 232, 0.60)',
20 | textWhite08: 'rgba(239, 237, 232, 0.80)',
21 | background05: ' rgba(4, 4, 4, 0.5)',
22 | };
23 |
24 | export default colors;
25 |
--------------------------------------------------------------------------------
/src/pages/Profile/Profile.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const Container = styled.div`
5 | padding: 40px 20px 78px 20px;
6 | ${mq.tablet} {
7 | padding: 72px 32px 54px 32px;
8 | }
9 | ${mq.desktop} {
10 | padding: 72px 96px 44px 96px;
11 | }
12 | `;
13 |
14 | export const BlockWrapper = styled.div`
15 | margin-top: 40px;
16 |
17 | ${mq.tablet} {
18 | margin-top: 64px;
19 | }
20 |
21 | ${mq.desktop} {
22 | display: flex;
23 | flex-direction: row-reverse;
24 | gap: 0 49px;
25 | margin-top: 32px;
26 | justify-content: space-between;
27 | }
28 | `;
29 |
30 | export const FormWrap = styled.div`
31 | ${mq.desktop} {
32 | padding-right: 64px;
33 | border-right: 1px solid ${colors.textWhite02};
34 | }
35 | `;
36 |
--------------------------------------------------------------------------------
/src/redux/statistic/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { toast } from 'react-toastify';
4 |
5 | axios.defaults.baseURL = 'https://power-pulse-rest-api.onrender.com';
6 |
7 | export const getVideoCountAndBurnedCaloriesStatistics = createAsyncThunk(
8 | 'getVideoCountAndBurnedCaloriesStatistics',
9 | async (_, { rejectWithValue }) => {
10 | try {
11 | const { data } = await axios.get('/api/statistics');
12 | return data;
13 | } catch (error) {
14 | toast.error('Oops... Something went wrong! Try again!');
15 | return rejectWithValue('Oops... Something went wrong!');
16 | }
17 | },
18 | {
19 | condition: (_, { getState }) => {
20 | const state = getState();
21 | if (state.statistics.allExercises > 1) {
22 | return false;
23 | }
24 | },
25 | },
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/Title/Title.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const StyledTitle = styled.h1`
5 | animation: backInLeft 1s ease-in-out alternate;
6 | color: ${colors.white};
7 |
8 | font-family: Roboto;
9 | font-size: 24px;
10 | font-style: normal;
11 | font-weight: 700;
12 | line-height: 1.05;
13 | letter-spacing: 0.38px;
14 | margin-bottom: ${props => props.margin?.bt?.mob || '0'}px;
15 | margin-top: ${props => props.margin?.top?.mob || '0'}px;
16 |
17 | ${mq.tablet} {
18 | margin-bottom: ${props => props.margin?.bt?.tab || '0'}px;
19 | margin-top: ${props => props.margin?.top?.tab || '0'}px;
20 | font-size: 32px;
21 |
22 | line-height: 1.11;
23 | letter-spacing: 0.7px;
24 | }
25 |
26 | ${mq.desktop} {
27 | margin-top: ${props => props.margin?.top?.desk || '0'}px;
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/SharedLayout/SharedLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { Outlet } from 'react-router-dom';
3 | import { Container } from './SheradLayout.styled';
4 | import Header from '../../components/headersComp/Header/Header';
5 | import Loader from '../../components/Lodaer/Loader';
6 | import { useSelector } from 'react-redux';
7 | import { isLoadingSignInOrSignUp } from '../../redux/auth/selectors';
8 | import LoaderForPages from '../LoaderForPages/LoaderForPages';
9 |
10 | const SharedLayout = () => {
11 | const isLoading = useSelector(isLoadingSignInOrSignUp);
12 |
13 | return (
14 | <>
15 | {isLoading && }
16 |
17 |
18 | }>
19 |
20 |
21 |
22 | >
23 | );
24 | };
25 |
26 | export default SharedLayout;
27 |
--------------------------------------------------------------------------------
/src/components/BtnSubtitle/BtnSubtitle.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Link } from 'react-router-dom';
3 | import colors from '../../utils/colorVeriables';
4 | import mq from '../../utils/mediaQuery';
5 |
6 | export const BtnSubtitleText = styled.span`
7 | color: ${colors.textWhite06};
8 | font-family: Roboto;
9 | font-size: 12px;
10 | line-height: 150%;
11 | `;
12 |
13 | export const BtnSubtitleLink = styled(Link)`
14 | color: ${colors.white};
15 | font-family: Roboto;
16 | font-size: 12px;
17 | line-height: 150%;
18 | text-decoration-line: underline;
19 | margin-left: 5px;
20 | transition: color 250ms cubic-bezier(0.075, 0.82, 0.165, 1);
21 | &:hover,
22 | &:focus {
23 | color: ${colors.orange};
24 | }
25 | `;
26 |
27 | export const TextWrapper = styled.div`
28 | display: inline-block;
29 | margin-top: 12px;
30 |
31 | ${mq.tablet} {
32 | padding-left: 9px;
33 | }
34 | `;
35 |
--------------------------------------------------------------------------------
/src/pages/Params/Params.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import ParamsForm from '../../components/ParamsForm';
3 | import ParamsBar from '../../components/ParamsBar';
4 | import { ParamsPageWrapper, ParamsPageWrapperDesktop } from './Params.styled';
5 |
6 | const Params = () => {
7 | const [swiperRef, setSwiperRef] = useState(null);
8 | const [steps, setSteps] = useState(1);
9 |
10 | useEffect(() => {
11 | if (!swiperRef) {
12 | return;
13 | }
14 |
15 | swiperRef.slideTo(steps - 1, 0);
16 | }, [steps, swiperRef]);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Params;
31 |
--------------------------------------------------------------------------------
/src/redux/products/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { addProduct } from './operations';
3 |
4 | const contactsInitialValue = {
5 | isLoading: false,
6 | error: null,
7 | };
8 |
9 | const handlePending = state => {
10 | state.isLoading = true;
11 | state.error = null;
12 | };
13 |
14 | const handleFullfield = state => {
15 | state.isLoading = false;
16 | state.error = null;
17 | };
18 |
19 | const handleRejected = (state, payload) => {
20 | state.isLoading = false;
21 | state.error = payload.error;
22 | };
23 |
24 | const products = createSlice({
25 | name: 'products',
26 | initialState: contactsInitialValue,
27 | extraReducers: builder => {
28 | builder.addCase(addProduct.pending, handlePending);
29 | builder.addCase(addProduct.fulfilled, handleFullfield);
30 | builder.addCase(addProduct.rejected, handleRejected);
31 | },
32 | });
33 |
34 | export const productsReducer = products.reducer;
35 |
--------------------------------------------------------------------------------
/src/hooks/useMatchMedia.js:
--------------------------------------------------------------------------------
1 | import { useState, useLayoutEffect } from 'react';
2 |
3 | const queries = [
4 | '(max-width: 767px)',
5 | '(min-width: 768px) and (max-width: 1439px)',
6 | '(min-width: 1440px)',
7 | ];
8 |
9 | export const useMatchMedia = () => {
10 | const mediaQueryLists = queries.map(query => matchMedia(query));
11 |
12 | const getValues = () => mediaQueryLists.map(list => list.matches);
13 |
14 | const [values, setValues] = useState(getValues);
15 |
16 | useLayoutEffect(() => {
17 | const handler = () => setValues(getValues);
18 |
19 | mediaQueryLists.forEach(list => list.addEventListener('change', handler));
20 |
21 | return () =>
22 | mediaQueryLists.forEach(list =>
23 | list.removeEventListener('change', handler),
24 | );
25 | });
26 |
27 | return ['isMobile', 'isTablet', 'isDesktop'].reduce(
28 | (acc, screen, index) => ({
29 | ...acc,
30 | [screen]: values[index],
31 | }),
32 | {},
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/ParamsBtn/ParamsBtn.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { mq, colors } from '../../utils';
4 |
5 | export const BtnNav = styled.button`
6 | display: inline-flex;
7 | justify-content: center;
8 | align-items: center;
9 | padding-top: 0;
10 | padding-bottom: 0;
11 | font-size: 14px;
12 | font-weight: 400;
13 | line-height: 128%;
14 |
15 | color: ${colors.white};
16 | background: transparent;
17 |
18 | border: none;
19 | outline: none;
20 |
21 | transition:
22 | color 250ms cubic-bezier(0.4, 0, 0.2, 1),
23 | transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
24 |
25 | &:hover {
26 | color: ${colors.orange};
27 | transform: scale(1.1);
28 | }
29 |
30 | &:focus {
31 | color: ${colors.orange};
32 | transform: scale(1.1);
33 | }
34 | ${mq.tablet} {
35 | font-size: 16px;
36 | line-height: 150%;
37 | }
38 | `;
39 |
40 | export const Svg = styled.svg`
41 | width: 20px;
42 | height: 20px;
43 | stroke: ${colors.orange};
44 | `;
45 |
--------------------------------------------------------------------------------
/src/components/Scrollbar/Scrollbar.syled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const ScrollContainer = styled.div`
5 | overflow: auto;
6 | height: calc(100vh - 48px);
7 | scrollbar-width: 0px;
8 |
9 | width: ${({ width }) => width?.mob + 'px' || '100%'};
10 |
11 | ${mq.tablet} {
12 | width: ${({ width }) => width?.tab + 'px' || '100%'};
13 | height: 550px;
14 | scrollbar-color: ${colors.orange} ${colors.textWhite01};
15 | scrollbar-width: thin;
16 | ::-webkit-scrollbar {
17 | width: 8px;
18 | background-color: ${colors.textWhite01};
19 | border-radius: 12px;
20 | }
21 | ::-webkit-scrollbar-thumb {
22 | background-color: ${colors.orange};
23 | border-radius: 12px;
24 | }
25 | ::-webkit-scrollbar-thumb:hover {
26 | background-color: ${colors.orange};
27 | }
28 | }
29 | ${mq.desktop} {
30 | width: ${({ width }) => width?.dt + 'px' || '100%'};
31 | height: 570px;
32 | }
33 | `;
34 |
--------------------------------------------------------------------------------
/src/components/MainTitle/MainTitle.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const Text = styled.h1`
5 | letter-spacing: 0.38px;
6 | font-weight: 500;
7 | color: ${colors.white};
8 |
9 | ${mq.smallMobile} {
10 | margin-top: 66px;
11 | margin-bottom: 40px;
12 | line-height: 105.26%;
13 | font-size: 38px;
14 | }
15 |
16 | ${mq.tablet} {
17 | width: 598px;
18 | font-size: 70px;
19 | line-height: 111.43%;
20 | letter-spacing: 0.7px;
21 |
22 | margin-top: 116px;
23 | margin-bottom: 64px;
24 | }
25 |
26 | & > span {
27 | position: relative;
28 | display: inline-block;
29 |
30 | & > svg {
31 | position: absolute;
32 | z-index: -1;
33 | width: 98px;
34 | height: 35px;
35 | left: -7px;
36 | top: 3px;
37 |
38 | ${mq.tablet} {
39 | width: 185px;
40 | height: 67px;
41 | left: -20px;
42 | top: 10px;
43 | }
44 | }
45 | }
46 | `;
47 |
--------------------------------------------------------------------------------
/src/utils/pageContentToRender.js:
--------------------------------------------------------------------------------
1 | import { capitalizeWord } from './capitalizeWord';
2 | const pageContentToRender = (page, data) => {
3 | const content =
4 | page === 'product'
5 | ? {
6 | subtitle: 'DIET',
7 | title: capitalizeWord(data.title),
8 | button: 'Add',
9 | text1: 'Calories:',
10 | text2: 'Category:',
11 | text3: 'Weight:',
12 | subText1: data.calories,
13 | subText2: capitalizeWord(data.category),
14 | subText3: data.weight,
15 | }
16 | : {
17 | subtitle: 'WORKOUT',
18 | title: capitalizeWord(data.name),
19 | button: 'Start',
20 | text1: 'Burned calories:',
21 | text2: 'Body part:',
22 | text3: 'Target:',
23 | subText1: data.burnedCalories,
24 | subText2: capitalizeWord(data.bodyPart),
25 | subText3: capitalizeWord(data.target),
26 | };
27 |
28 | return content;
29 | };
30 |
31 | export default pageContentToRender;
32 |
--------------------------------------------------------------------------------
/src/components/BtnSubmit/BtnSubmit.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const ButtonSubmit = styled.button`
5 | display: inline-flex;
6 | align-items: center;
7 | justify-content: center;
8 | padding: 12px 40px;
9 | outline: none;
10 | background-color: ${colors.orange};
11 | border-radius: 12px;
12 | border: none;
13 | margin-top: ${({ margin }) => {
14 | return margin?.top.mob || 0;
15 | }};
16 | margin-bottom: ${({ margin }) => {
17 | return margin?.bot.mob || 0;
18 | }};
19 |
20 | color: ${colors.white};
21 | font-size: 16px;
22 | font-weight: 500;
23 | line-height: 112.5%;
24 | transition: background 0.3s ease-out;
25 |
26 | ${mq.tablet} {
27 | padding: 14px 32px;
28 | line-height: 120%;
29 | margin-top: ${({ margin }) => margin?.top.tab || 0};
30 | margin-bottom: ${({ margin }) => margin?.bot.tab || 0};
31 | }
32 |
33 | &:hover,
34 | &:focus {
35 | background: ${colors.orangeSecondary};
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/src/components/DescriptionText/DescriptionText.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const ExclamationText = styled.p`
5 | width: auto;
6 | display: flex;
7 | margin-top: ${props => props.margin.top.mobile}px;
8 | margin-bottom: ${props => props.margin.bottom.mobile}px;
9 |
10 | font-size: 14px;
11 | line-height: 1.29;
12 |
13 | color: ${colors.textWhite03};
14 |
15 | ${mq.tablet} {
16 | width: ${props => props.width.tablet}px;
17 | margin-top: ${props => props.margin.top.tablet}px;
18 | margin-bottom: ${props => props.margin.bottom.tablet}px;
19 |
20 | font-size: 16px;
21 | line-height: 1.5;
22 | }
23 |
24 | ${mq.desktop} {
25 | width: ${props => props.width.desktop}px;
26 | margin-top: ${props => props.margin.top.desktop}px;
27 | margin-bottom: ${props => props.margin.bottom.desktop}px;
28 | }
29 | `;
30 |
31 | export const ExclamationIcon = styled.svg`
32 | max-width: 24px;
33 | max-height: 24px;
34 | margin-right: 8px;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/components/DailyStatsCards/DailyStatsCards.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import sprite from '../../assets/sprite.svg';
3 | import {
4 | KeyWrap,
5 | CardWrap,
6 | Svg,
7 | KeyValue,
8 | Label,
9 | } from './DailyStatsCards.styled';
10 |
11 | const DailyStatsCards = ({
12 | icon,
13 | keyValue,
14 | label,
15 | border = 'default',
16 | fill = 'false',
17 | }) => {
18 | return (
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 | {keyValue}
30 |
31 | );
32 | };
33 |
34 | // DailyStatsCards.propTypes = {
35 | // icon: PropTypes.string.isRequired,
36 | // keyValue: PropTypes.any.isRequired,
37 | // label: PropTypes.string.isRequired,
38 | // border: PropTypes.oneOf(['green', 'red', "default"]),
39 | // fill: PropTypes.oneOf(['true', 'false']),
40 | // };
41 |
42 | export default DailyStatsCards;
43 |
--------------------------------------------------------------------------------
/src/components/headersComp/Header/Header.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom';
2 | import { HeaderContainer } from './Header.styled';
3 | import UserNav from '../UserNav/UserNav';
4 | import Logo from '../Logo/Logo';
5 | import MobMenu from '../../MobMenu/MobMenu';
6 |
7 | import { UseAuth } from '../../../hooks/useAuth';
8 |
9 | export const Header = () => {
10 | const { isLoggedIn } = UseAuth();
11 | const { pathname } = useLocation();
12 | const isMatchingRoute = [
13 | '/',
14 | '/signin',
15 | '/signup',
16 | '/params',
17 | '/profile',
18 | '/diary',
19 | '/exercises',
20 | '/products',
21 | ].some(route => pathname === route);
22 |
23 | const isBorderToRender = isLoggedIn && pathname !== '/params';
24 |
25 | if (!isMatchingRoute) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 |
32 | {isLoggedIn && pathname !== '/params' && (
33 | <>
34 |
35 |
36 | >
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default Header;
43 |
--------------------------------------------------------------------------------
/src/components/Timer/Timer.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors } from '../../utils';
3 |
4 | export const FlexContainer = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | `;
9 |
10 | export const TimerTitle = styled.div`
11 | margin-bottom: 4px;
12 | font-size: 10px;
13 | line-height: 140%;
14 | color: ${colors.textWhite04};
15 | `;
16 |
17 | export const PauseButton = styled.button`
18 | margin-bottom: 8px;
19 | border: none;
20 | outline: none;
21 | padding: 0;
22 | background-color: transparent;
23 |
24 | &:hover svg {
25 | scale: 1.15;
26 | }
27 |
28 | `;
29 |
30 | export const Svg = styled.svg`
31 | width: 32px;
32 | height: 32px;
33 | fill: ${colors.orange};
34 | stroke: ${colors.white};
35 |
36 | scale: 1;
37 |
38 | transition: scale 250ms cubic-bezier(0.4, 0, 0.2, 1);
39 | `;
40 |
41 | export const BurntCaloryLabel = styled.div`
42 | font-size: 14px;
43 | line-height: 128%;
44 | color: ${colors.textWhite03};
45 | `;
46 |
47 | export const BurntCaloryInfo = styled.span`
48 | margin-left: 8px;
49 | color: ${colors.orange};
50 | `;
51 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | CheckboxWrap,
3 | InputHidden,
4 | Chkbx,
5 | CheckboxText,
6 | } from "./Checkbox.styled";
7 | import PropTypes from "prop-types";
8 |
9 | export default function Checkbox({
10 | styleWrapper,
11 | styleCheckbox,
12 | styleText,
13 | name,
14 | value,
15 | checked,
16 | onChange,
17 | children,
18 | }) {
19 | return (
20 |
21 |
28 |
29 |
30 |
31 | {children}
32 |
33 | );
34 | }
35 |
36 | Checkbox.propTypes = {
37 | styleWrapper: PropTypes.object,
38 | styleCheckbox: PropTypes.object,
39 | styleText: PropTypes.object,
40 | name: PropTypes.string.isRequired,
41 | value: PropTypes.string.isRequired,
42 | checked: PropTypes.oneOf([true, false]),
43 | onChange: PropTypes.func,
44 | children: PropTypes.string,
45 | };
46 |
--------------------------------------------------------------------------------
/src/redux/exerciseFilters/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { fetchFilters } from './operations';
3 |
4 | export const filterSlice = createSlice({
5 | name: 'filters',
6 | initialState: {
7 | items: [],
8 | error: null,
9 | isLoading: false,
10 | filter: 'Body parts',
11 | currentTitle: 'Exersises',
12 | },
13 |
14 | reducers: {
15 | setStatusFilter: (state, action) => {
16 | state.filter = action.payload;
17 | },
18 | setCurrentTitle: (state, action) => {
19 | state.currentTitle = action.payload;
20 | },
21 | },
22 | extraReducers: builder => {
23 | builder.addCase(fetchFilters.fulfilled, (state, action) => {
24 | state.items = action.payload;
25 | state.error = null;
26 | state.isLoading = false;
27 | });
28 | builder.addCase(fetchFilters.rejected, (state, action) => {
29 | state.error = action.payload;
30 | state.isLoading = false;
31 | });
32 | builder.addCase(fetchFilters.pending, state => {
33 | state.isLoading = true;
34 | });
35 | },
36 | });
37 | export const { setStatusFilter, setCurrentTitle } = filterSlice.actions;
38 | export default filterSlice.reducer;
39 |
--------------------------------------------------------------------------------
/src/redux/exercises/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 |
4 | export const getExercises = createAsyncThunk(
5 | 'exercises/getExercises',
6 | async (params, thunkAPI) => {
7 | try {
8 | const response = await axios.get(`api/exercises?${params}`);
9 | return response.data;
10 | } catch (error) {
11 | return thunkAPI.rejectWithValue(error.message);
12 | }
13 | },
14 | );
15 |
16 | export const addExercise = createAsyncThunk(
17 | 'addExercise',
18 | async (exerciseDetails, thunkAPI) => {
19 | try {
20 | const { data } = await axios.post(
21 | '/api/diary/add-exercise',
22 | exerciseDetails,
23 | );
24 | return data;
25 | } catch (error) {
26 | return thunkAPI.rejectWithValue(error.message);
27 | }
28 | },
29 | );
30 |
31 | export const getMoreExercises = createAsyncThunk(
32 | 'exercises/getMoreExercises',
33 | async (params, thunkAPI) => {
34 | try {
35 | const response = await axios.get(`api/exercises?${params}`);
36 | return response.data;
37 | } catch (error) {
38 | return thunkAPI.rejectWithValue(error.message);
39 | }
40 | },
41 | );
42 |
--------------------------------------------------------------------------------
/src/components/ExercisesBtnBack/ExercisesBtnBack.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { mq, colors } from '../../utils';
4 |
5 | export const ButtonBack = styled.button`
6 | display: flex;
7 | align-items: center;
8 |
9 | padding: 0px;
10 | margin-top: 10px;
11 |
12 | font-size: 14px;
13 | line-height: 1.28;
14 |
15 | background-color: transparent;
16 | color: ${colors.textWhite04};
17 | border: none;
18 | scale: 1;
19 |
20 | transition:
21 | scale 250ms ease-in-out,
22 | color 250ms ease-in-out,
23 | fill 250ms ease-in-out;
24 |
25 | ${mq.tablet} {
26 | font-size: 16px;
27 | line-height: 1.5;
28 | }
29 |
30 | &:hover {
31 | color: ${colors.orange};
32 | scale: 1.1;
33 | }
34 | &:focus {
35 | color: ${colors.orange};
36 | scale: 1.1;
37 | }
38 |
39 | &:hover svg {
40 | fill: ${colors.orange};
41 | scale: 1.1;
42 | }
43 | &:focus svg {
44 | fill: ${colors.orange};
45 | scale: 1.1;
46 | }
47 | `;
48 |
49 | export const SvgArrow = styled.svg`
50 | width: 16px;
51 | height: 16px;
52 | margin-right: 8px;
53 | fill: ${colors.textWhite04};
54 | scale: 1;
55 |
56 | transition:
57 | scale 250ms ease-in-out,
58 | fill 250ms ease-in-out;
59 | `;
60 |
--------------------------------------------------------------------------------
/src/pages/Profile/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import Title from '../../components/Title/Title';
3 | import UserCard from '../../components/UserCard';
4 | import UserForm from '../../components/UserForm';
5 | import { BlockWrapper, FormWrap, Container } from './Profile.styled';
6 | import { useDispatch } from 'react-redux';
7 | import { updateUserData } from '../../redux/auth/operation';
8 |
9 | export default function Profile() {
10 | const [avatarFile, setAvatarFile] = useState(null);
11 | const dispatch = useDispatch();
12 |
13 | const handleSubmit = data => {
14 | const formData = new FormData();
15 | Object.entries(data).forEach(value => {
16 | formData.append(value[0], value[1]);
17 | });
18 | avatarFile && formData.append('avatar', avatarFile, avatarFile.name);
19 |
20 | dispatch(updateUserData(formData));
21 | setAvatarFile(null);
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Backdrop, ModalWrap, ButtonExit, Svg } from './Modal.styled';
3 | import sprite from '../../assets/sprite.svg';
4 | import { createPortal } from 'react-dom';
5 |
6 | const modalRoot = document.querySelector('#modal-root');
7 |
8 | export const Modal = ({ children, openModal, width, height }) => {
9 | useEffect(() => {
10 | const handleKeyDown = e => {
11 | if (e.code === 'Escape') {
12 | openModal();
13 | }
14 | };
15 |
16 | window.addEventListener('keydown', handleKeyDown);
17 |
18 | return () => {
19 | window.removeEventListener('keydown', handleKeyDown);
20 | };
21 | }, [openModal]);
22 |
23 | const handleBackdropClick = event => {
24 | if (event.currentTarget === event.target) {
25 | openModal();
26 | }
27 | };
28 |
29 | return createPortal(
30 |
31 |
32 | openModal()}>
33 |
36 |
37 |
38 | {children}
39 |
40 | ,
41 | modalRoot,
42 | );
43 | };
44 |
45 | export default Modal;
46 |
47 |
--------------------------------------------------------------------------------
/src/components/DayDiary/DayDiary.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import {
4 | DayDiaryContainer,
5 | DayDiarySubTitle,
6 | AddLink,
7 | DayNoContentText,
8 | ArrowRight,
9 | DayDiarySubDiv,
10 | } from './DayDiary.styled';
11 | import sprite from '../../assets/sprite.svg';
12 | import ProductsTable from '../ProductsTable/ProductsTable';
13 |
14 | const DayDiary = ({ products, to, isDayProducts }) => {
15 | return (
16 |
17 |
18 |
19 | {isDayProducts ? 'Products' : 'Exercises'}
20 |
21 |
22 | Add {isDayProducts ? 'product' : 'exercise'}
23 |
24 |
25 |
26 |
27 |
28 | {products.length !== 0 ? (
29 |
30 | ) : (
31 |
32 | Not found {isDayProducts ? 'products' : 'exercises'}
33 |
34 | )}
35 |
36 | );
37 | };
38 |
39 | DayDiary.propTypes = {
40 | to: PropTypes.string,
41 | isDayProducts: PropTypes.string,
42 | products: PropTypes.array,
43 | };
44 |
45 | export default DayDiary;
46 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, configureStore } from '@reduxjs/toolkit';
2 | import {
3 | persistStore,
4 | persistReducer,
5 | FLUSH,
6 | REHYDRATE,
7 | PAUSE,
8 | PERSIST,
9 | PURGE,
10 | REGISTER,
11 | } from 'redux-persist';
12 | import storage from 'redux-persist/lib/storage';
13 |
14 | import { authSlice } from './auth/slice';
15 | import filterSlice from './exerciseFilters/slice';
16 | import exercisesSlice from './exercises/slice';
17 | import { diaryReducer } from './diary/slice';
18 | import productsSlice from './productsFilter/slice';
19 | import { statisticsReducer } from './statistic/slice';
20 |
21 | const persistConfig = {
22 | key: 'token',
23 | storage,
24 | whitelist: ['token'],
25 | };
26 |
27 | const rootReducer = combineReducers({
28 | auth: persistReducer(persistConfig, authSlice.reducer),
29 | filter: filterSlice,
30 | exercises: exercisesSlice,
31 | diary: diaryReducer,
32 | products: productsSlice,
33 | statistics: statisticsReducer,
34 | });
35 |
36 | export const store = configureStore({
37 | reducer: rootReducer,
38 | middleware: getDefaultMiddleware =>
39 | getDefaultMiddleware({
40 | serializableCheck: {
41 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
42 | },
43 | }),
44 | });
45 |
46 | export const persistor = persistStore(store);
47 |
--------------------------------------------------------------------------------
/src/redux/productsFilter/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 |
4 | export const fetchProducts = createAsyncThunk(
5 | `filter/getProducts`,
6 | async (searchParams = '', thunkAPI) => {
7 | try {
8 | const res = await axios.get(`api/products?${searchParams}`);
9 | return res.data;
10 | } catch (error) {
11 | return thunkAPI.rejectWithValue(error.message);
12 | }
13 | },
14 | );
15 |
16 | export const getCategories = createAsyncThunk(
17 | `categories/getCategories`,
18 | async (_, thunkAPI) => {
19 | try {
20 | const res = await axios.get('api/categories');
21 |
22 | return res.data[0].categories;
23 | } catch (error) {
24 | return thunkAPI.rejectWithValue(error.message);
25 | }
26 | },
27 | {
28 | condition: (_, { getState, extra }) => {
29 | const state = getState();
30 | if (state.products.categories.length > 1) {
31 | return false;
32 | }
33 | },
34 | },
35 | );
36 |
37 | export const fetchMoreProducts = createAsyncThunk(
38 | `fetchMoreProducts`,
39 | async (params, thunkAPI) => {
40 | try {
41 | const { data } = await axios.get(`api/products?${params}`);
42 | return data;
43 | } catch (error) {
44 | return thunkAPI.rejectWithValue(error.message);
45 | }
46 | },
47 | );
48 |
--------------------------------------------------------------------------------
/src/components/ExercisesItem/ExercisesItem.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {
3 | Item,
4 | WrapCard,
5 | Image,
6 | WrapSpan,
7 | Name,
8 | Filter,
9 | } from './ExercisesItem.styled';
10 | import { useEffect, useState } from 'react';
11 |
12 | export const ExercisesItem = ({ filter, name, imgURL, handleGetExercises }) => {
13 | const [query, setQuery] = useState(filter);
14 |
15 | useEffect(() => {
16 | switch (filter) {
17 | case 'Body parts':
18 | return setQuery('bodyPart');
19 | case 'Muscles':
20 | return setQuery('target');
21 | case 'Equipment':
22 | return setQuery('equipment');
23 | default:
24 | return;
25 | }
26 | }, [filter]);
27 |
28 | const requestString = `${query}=${name.toLowerCase()}`;
29 |
30 | return (
31 | - handleGetExercises(requestString, name)}>
32 |
33 |
34 |
35 | {name}
36 | {filter}
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default ExercisesItem;
44 |
45 | ExercisesItem.propTypes = {
46 | filter: PropTypes.string.isRequired,
47 | name: PropTypes.string.isRequired,
48 | imgURL: PropTypes.string.isRequired,
49 | handleGetExercises: PropTypes.func.isRequired,
50 | };
51 |
--------------------------------------------------------------------------------
/src/pages/Products/Products.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils';
3 |
4 | import imgProducts from '../../assets/images/imgProduct';
5 |
6 | export const ProductPageContainer = styled.div`
7 | display: flex;
8 | flex-direction: column;
9 | padding: 40px 20px 0px 20px;
10 | gap: 40px;
11 | position: relative;
12 |
13 | ${mq.tablet} {
14 | gap: 32px;
15 | padding: 72px 32px 0px 32px;
16 | }
17 |
18 | ${mq.desktop} {
19 | padding: 68px 81px 0px 96px;
20 |
21 | gap: 28px;
22 |
23 | background: linear-gradient(89deg, #040404 1.1%, rgba(4, 4, 4, 0) 70.79%);
24 | background-repeat: no-repeat;
25 | background-position: 100% 0;
26 | background-size: 428px 716px;
27 | background-image: url(${imgProducts.imgDx1});
28 |
29 | ${
30 | '' /* @media (min-device-pixel-ratio: 2),
31 | (-webkit-min-device-pixel-ratio: 2),
32 | (min-resolution: 192dpi),
33 | (min-resolution: 2dppx) {
34 | & {
35 | background-image: url(${imgProducts.imgDx2});
36 | }
37 |
38 |
39 | } */
40 | }
41 | }
42 | `;
43 |
44 | export const FlexWrapper = styled.div`
45 | display: flex;
46 | gap: 40px;
47 | flex-direction: column;
48 | ${mq.tablet} {
49 | gap: 32px;
50 | }
51 | ${mq.desktop} {
52 | flex-direction: row;
53 | justify-content: space-between;
54 | }
55 | `;
56 |
--------------------------------------------------------------------------------
/src/pages/Exercises/Exercises.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq } from '../../utils';
3 |
4 | import imgDx2 from '../../assets/images/exercises_desk_2x.jpeg';
5 |
6 | export const ExercisesContainer = styled.div`
7 | padding-left: 20px;
8 | padding-right: 20px;
9 |
10 | ${mq.tablet} {
11 | padding-left: 32px;
12 | padding-right: 32px;
13 | }
14 |
15 | ${mq.desktop} {
16 | padding-left: 95px;
17 | padding-right: 95px;
18 | }
19 | `;
20 |
21 | export const TitleThumb = styled.div`
22 | ${mq.tablet} {
23 | display: flex;
24 | align-items: baseline;
25 | justify-content: space-between;
26 | }
27 | `;
28 |
29 | export const ExercisesListContainer = styled.div`
30 | padding: 40px 20px 80px 20px;
31 | position: relative;
32 |
33 |
34 | ${mq.tablet} {
35 | padding: 32px 32px 0px 32px;
36 | }
37 |
38 | ${mq.desktop} {
39 | padding: 0px 81px 0px 96px;
40 | }
41 | `;
42 |
43 | export const BGImg = styled.div`
44 | ${mq.desktop} {
45 | position: absolute;
46 | top: -117px;
47 | right: 0;
48 | z-index: -1;
49 | display: block;
50 |
51 | width: 428px;
52 | height: 716px;
53 | background: linear-gradient(89deg, #040404 1.1%, rgba(4, 4, 4, 0) 70.79%);
54 | background-repeat: no-repeat;
55 | background-position: 100% 0;
56 | background-size: 428px 716px;
57 | background-image: url(${imgDx2});
58 | }
59 | `;
--------------------------------------------------------------------------------
/src/components/ExercisesCategories/ExercisesCategories.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const CategoriesList = styled.ul`
5 | display: flex;
6 | gap: 28px;
7 |
8 | margin-top: 20px;
9 |
10 | ${mq.tablet} {
11 | gap: 32px;
12 | }
13 | `;
14 | export const CategoriesListItem = styled.li``;
15 |
16 | export const CategoriesLink = styled.button`
17 | position: relative;
18 | padding: 0;
19 |
20 | color: ${colors.textWhite03};
21 | font-family: Roboto;
22 | font-size: 14px;
23 |
24 | line-height: 1.28;
25 |
26 | background-color: transparent;
27 | outline: none;
28 | border: none;
29 |
30 | &:after {
31 | content: '';
32 | display: block;
33 | position: absolute;
34 |
35 | transition:
36 | background-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
37 | transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
38 | transform: scalex(0);
39 |
40 | bottom: -4px;
41 | left: 0;
42 | background-color: transparent;
43 | }
44 |
45 | &.active::after {
46 | content: '';
47 | display: block;
48 | position: absolute;
49 | width: 100%;
50 | height: 4px;
51 |
52 | bottom: -4px;
53 | left: 0;
54 | background-color: #ef8964;
55 | transform: scalex(1);
56 | border-radius: 2px;
57 | }
58 |
59 | ${mq.tablet} {
60 | font-size: 16px;
61 |
62 | line-height: 1.5;
63 | }
64 | `;
65 |
--------------------------------------------------------------------------------
/src/components/headersComp/UserNav/UserNav.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors, button, svgUser } from '../../../utils';
3 |
4 | export const WrapUserNav = styled.nav`
5 | ${mq.smallMobile} {
6 | display: none;
7 | }
8 | ${mq.desktop} {
9 | display: flex;
10 | align-items: center;
11 | gap: 16px;
12 | margin-left: auto;
13 | }
14 | `;
15 | export const Svg = styled.svg`
16 | width: 24px;
17 | height: 24px;
18 | stroke: currentColor;
19 | transition: scale 250ms cubic-bezier(0.4, 0, 0.2, 1);
20 |
21 | &:hover,
22 | &:focus {
23 | scale: 1.2;
24 | }
25 | `;
26 | export const SvgUser = styled.svg`
27 | ${svgUser}
28 | `;
29 | export const ButtonWrap = styled.div`
30 | display: flex;
31 | align-items: center;
32 | margin-left: 30px;
33 | gap: 16px;
34 | `;
35 |
36 | export const Button = styled.button`
37 | display: flex;
38 | align-items: center;
39 | gap: 5px;
40 | color: ${colors.orange};
41 | ${button};
42 | `;
43 | export const Span = styled.span`
44 | font-size: 16px;
45 | line-height: 24px;
46 |
47 | ${mq.desktop} {
48 | gap: 8px;
49 | }
50 | `;
51 |
52 | export const UserAvatar = styled.div`
53 | width: 46px;
54 | height: 46px;
55 | border-radius: 50%;
56 | overflow: hidden;
57 | border: 1px solid ${colors.orange};
58 | display: flex;
59 | align-items: center;
60 | justify-content: center;
61 |
62 | > img {
63 | width: 100%;
64 | height: auto;
65 | }
66 | `;
67 |
--------------------------------------------------------------------------------
/src/pages/Error/Error.jsx:
--------------------------------------------------------------------------------
1 | import CustomBtn from '../../components/CustomNavLink/CustomNavLink';
2 | import {
3 | Container,
4 | Content,
5 | Title,
6 | Text,
7 | BGImg,
8 | WrapLogo,
9 | Logo,
10 | LogoText,
11 | } from './Error.styled';
12 | import sprite from '../../assets/sprite.svg';
13 | import { UseAuth } from '../../hooks/useAuth';
14 |
15 | const Error = () => {
16 | const { isLoggedIn } = UseAuth();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 404
31 |
32 | Sorry, you have reached a page that we could not find. It seems that
33 | you are lost among the numbers and letters of our virtual space.
34 | Perhaps this page went on vacation or decided to disappear into
35 | another dimension. We apologize for this inconvenience.
36 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default Error;
53 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors } from '../../utils';
3 |
4 | export const CheckboxWrap = styled.label`
5 | display: inline-flex;
6 | align-items: center;
7 | position: relative;
8 |
9 | &:not(:last-child) {
10 | margin-right: 9px;
11 | }
12 |
13 | &:hover p,
14 | &:focus p {
15 | color: ${colors.orange};
16 | border-color: ${colors.orange};
17 | }
18 | `;
19 |
20 | export const InputHidden = styled.input`
21 | position: absolute;
22 | width: 0;
23 | height: 0;
24 | opacity: 0;
25 | visibility: hidden;
26 |
27 | &:checked + p {
28 | border-color: #ef8964;
29 |
30 | &:before {
31 | background-color: #ef8964;
32 | }
33 | }
34 | `;
35 |
36 | export const Chkbx = styled.p`
37 | position: relative;
38 | display: inline-block;
39 | flex-shrink: 0;
40 | flex-grow: 0;
41 | width: 14px;
42 | height: 14px;
43 | background-color: #000;
44 | border: 1px solid #636366;
45 | border-radius: 50%;
46 | transition:
47 | color 0.3s,
48 | border-color 0.3s;
49 |
50 | &:before {
51 | content: '';
52 | position: absolute;
53 | bottom: 2px;
54 | left: 2px;
55 | width: 8px;
56 | height: 8px;
57 | background-color: #000;
58 | border-radius: 50%;
59 | }
60 | `;
61 |
62 | export const CheckboxText = styled.p`
63 | display: inline-block;
64 | margin-left: 8px;
65 | font-size: 14px;
66 | line-height: 1.3;
67 | color: grey;
68 | transition:
69 | color 0.3s,
70 | border-color 0.3s;
71 | `;
72 |
--------------------------------------------------------------------------------
/src/components/CustomInputForCalendar/CustomInputForCalendar.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const Label = styled.label`
5 | position: relative;
6 |
7 | background-color: inherit;
8 | border: none;
9 |
10 | cursor: pointer;
11 | &:hover svg,
12 | &:hover input {
13 | stroke: ${colors.orange};
14 | color: ${colors.orange};
15 | }
16 |
17 | &:focus-visible {
18 | border: none;
19 | }
20 |
21 | &:focus {
22 | border: none;
23 | }
24 | `;
25 |
26 | export const Input = styled.input`
27 | width: 125px;
28 |
29 | font-size: 18px;
30 | font-weight: 700;
31 | line-height: 1.11;
32 |
33 | cursor: pointer;
34 | color: ${colors.white};
35 | border: none;
36 | background-color: inherit;
37 |
38 | transition: color 0.3s;
39 |
40 | &:focus {
41 | color: ${colors.orange};
42 | }
43 |
44 | &:focus-visible {
45 | outline: none;
46 | }
47 |
48 | &:focus + svg {
49 | stroke: ${colors.orange};
50 | }
51 |
52 | ${mq.tablet} {
53 | width: 161px;
54 |
55 | font-size: 24px;
56 | line-height: 1.33;
57 | }
58 |
59 | ${mq.desktop} {
60 | }
61 | `;
62 |
63 | export const Icon = styled.svg`
64 | width: 18px;
65 | height: 18px;
66 | position: absolute;
67 | right: -8px;
68 | top: 46%;
69 |
70 | stroke: ${colors.grey};
71 |
72 | transform: translate(-50%, -50%);
73 | transition: stroke 0.3s;
74 |
75 | ${mq.tablet} {
76 | width: 24px;
77 | height: 24px;
78 | right: -14px;
79 | top: 40%;
80 | }
81 | `;
82 |
--------------------------------------------------------------------------------
/src/components/CustomNavLink/CustomNavLink.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import { colors, mq } from '../../utils';
5 |
6 | export const Link = styled(NavLink)`
7 | ${mq.smallMobile} {
8 | font-size: 14px;
9 | }
10 | ${mq.mobile} {
11 | font-size: 16px;
12 | }
13 |
14 | box-sizing: border-box;
15 | padding: ${props =>
16 | props.isinheader === 'true' ? '10px 27px' : '12px 40px'};
17 | display: inline-block;
18 | gap: 10px;
19 | border-radius: 12px;
20 |
21 | background-color: ${props =>
22 | props.isorange === 'true' ? colors.orange : 'inherit'};
23 | border: ${props =>
24 | props.isorange === 'true' ? 'none' : '1px solid rgba(239, 237, 232, 0.30)'};
25 | color: ${colors.white};
26 |
27 | font-weight: ${props => (props.isinheader === 'true' ? '400' : '500')};
28 | line-height: 1.13;
29 |
30 | &.active {
31 | background-color: ${colors.orange};
32 | }
33 |
34 | &.active:hover {
35 | background-color: ${colors.orange};
36 | }
37 |
38 | transition:
39 | background-color 0.3s,
40 | border 0.3s;
41 |
42 | &:hover {
43 | background-color: ${props =>
44 | props.isorange === 'true' ? colors.orangeSecondary : 'inherit'};
45 | border: ${props =>
46 | props.isorange === 'true' ? 'none' : '1px solid #E6533C'};
47 | }
48 |
49 | ${mq.tablet} {
50 | padding: ${props =>
51 | props.isinheader === 'true' ? '10px 27px' : '16px 60px'};
52 |
53 | font-size: ${props => (props.isinheader === 'true' ? '16px' : '20px')};
54 | line-height: 1.2;
55 | }
56 | `;
57 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, button, mq } from '../../utils';
3 |
4 | export const Backdrop = styled.div`
5 | position: fixed;
6 | width: 100%;
7 | height: 100%;
8 | left: 0;
9 | top: 0;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | background-color: ${colors.backdrop};
14 | `;
15 |
16 | export const ModalWrap = styled.div`
17 | border: 1px solid ${colors.textWhite02};
18 | position: absolute;
19 |
20 | overflow: auto;
21 | max-height: ${props => props.height[0]}px;
22 |
23 | display: flex;
24 | justify-content: center;
25 |
26 | ${mq.smallMobile} {
27 | width: 300px;
28 | }
29 | ${mq.mobile} {
30 | width: 335px;
31 | }
32 | padding: 40px 20px;
33 | border-radius: 8px;
34 | background-color: ${colors.modalBlack};
35 | z-index: 1;
36 |
37 | ${mq.tablet} {
38 | width: ${props => props.width}px;
39 | max-height: ${props => props.height[1]}px;
40 |
41 | padding: 40px 32px;
42 | }
43 | `;
44 |
45 | export const ButtonExit = styled.button`
46 | position: absolute;
47 | top: 14px;
48 | right: 14px;
49 | ${button}
50 |
51 | &:hover svg {
52 | stroke: ${colors.orange};
53 | scale: 1.1;
54 | }
55 | &:focus svg {
56 | stroke: ${colors.orange};
57 | scale: 1.1;
58 | }
59 | `;
60 |
61 | export const Svg = styled.svg`
62 | width: 24px;
63 | height: 24px;
64 | stroke: ${colors.textWhite04};
65 | scale: 1;
66 |
67 | transition:
68 | scale 200ms cubic-bezier(0.4, 0, 0.2, 1),
69 | stroke 200ms cubic-bezier(0.4, 0, 0.2, 1);
70 | `;
71 |
--------------------------------------------------------------------------------
/src/components/DayDiary/DayDiary.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import { colors, mq } from '../../utils';
5 |
6 | export const DayDiaryContainer = styled.div`
7 | position: relative;
8 | box-sizing: border-box;
9 | display: block;
10 | width: 100%;
11 | height: 335px;
12 | padding: 16px;
13 | align-items: flex-start;
14 |
15 | font-size: 14px;
16 | line-heigth: 1.29;
17 |
18 | border-radius: 12px;
19 | border: 1px solid ${colors.textWhite02};
20 | background: rgba(239, 237, 232, 0.05);
21 |
22 | ${mq.mobile} {
23 | width: 335px;
24 | }
25 |
26 | ${mq.tablet} {
27 | width: 704px;
28 | height: 234px;
29 | }
30 |
31 | ${mq.desktop} {
32 | width: 826px;
33 | }
34 | `;
35 |
36 | export const DayDiarySubDiv = styled.div`
37 | display: flex;
38 | justify-content: space-between;
39 | `;
40 |
41 | export const DayDiarySubTitle = styled.p`
42 | margin: 0;
43 |
44 | line-height: 1.29;
45 |
46 | color: ${colors.textWhite05};
47 | `;
48 |
49 | export const AddLink = styled(NavLink)`
50 | display: flex;
51 | align-items: center;
52 |
53 | color: ${colors.orange};
54 |
55 | ${mq.tablet} {
56 | font-size: 16px;
57 | font-weight: 500;
58 | line-height: 1.5;
59 | }
60 | `;
61 |
62 | export const DayNoContentText = styled.p`
63 | position: absolute;
64 | top: 50%;
65 | left: 50%;
66 | transform: translate(-50%, -50%);
67 |
68 | color: ${colors.textWhite05};
69 | `;
70 |
71 | export const ArrowRight = styled.svg`
72 | width: 20px;
73 | height: 20px;
74 | margin-left: 8px;
75 | `;
76 |
--------------------------------------------------------------------------------
/src/assets/images/favicon/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ExercisesItem/ExercisesItem.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils/index';
3 |
4 | export const Item = styled.li`
5 | margin-bottom: 20px;
6 |
7 | &:last-child {
8 | margin-bottom: 0px;
9 | }
10 |
11 | ${mq.tablet} {
12 | flex-basis: calc((100% - 32px) / 3);
13 | margin-bottom: 0;
14 |
15 | &:last-child {
16 | margin-bottom: 0;
17 | }
18 | }
19 |
20 | ${mq.desktop} {
21 | flex-basis: calc((100% - 64px) / 5);
22 | }
23 | `;
24 |
25 | export const WrapCard = styled.a`
26 | display: block;
27 |
28 | cursor: pointer;
29 | position: relative;
30 | border-radius: 12px;
31 | background-color: ${colors.background05};
32 | `;
33 |
34 | export const Image = styled.img`
35 | position: relative;
36 | width: 100%;
37 | border-radius: 12px;
38 | border: 1px solid rgba(239, 237, 232, 0.2);
39 |
40 | z-index: -1;
41 | `;
42 |
43 | export const WrapSpan = styled.div`
44 | position: absolute;
45 | top: 50%;
46 | left: 50%;
47 | transform: translate(-50%, -50%);
48 |
49 | display: flex;
50 | flex-direction: column;
51 | align-items: center;
52 |
53 | width: 137px;
54 | text-align: center;
55 |
56 | ${mq.mobile} {
57 | top: 50%;
58 | left: 50%;
59 | }
60 |
61 | ${mq.tablet} {
62 | top: 50%;
63 | left: 50%;
64 | }
65 | `;
66 |
67 | export const Name = styled.span`
68 | color: ${colors.white};
69 | font-size: 20px;
70 | line-height: 1.2;
71 |
72 | ${mq.tablet} {
73 | font-size: 24px;
74 | line-height: 1.33;
75 | }
76 | `;
77 |
78 | export const Filter = styled.span`
79 | color: ${colors.textWhite04};
80 | font-size: 12px;
81 | line-height: 1.5;
82 | `;
83 |
--------------------------------------------------------------------------------
/src/redux/statistic/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { getVideoCountAndBurnedCaloriesStatistics } from './operations';
3 | import formatNumber from '../../utils/formatNumberStatistics';
4 |
5 | const contactsInitialValue = {
6 | isLoading: false,
7 | error: null,
8 | allExercises: 0,
9 | allUsers: 0,
10 | usersBurnedCalories: 0,
11 | usersTimeTraining: 0,
12 | usersTraining: 0,
13 | };
14 |
15 | const handlePending = state => {
16 | state.isLoading = true;
17 | state.error = null;
18 | };
19 |
20 | const handleFullfield = state => {
21 | state.isLoading = false;
22 | state.error = null;
23 | };
24 |
25 | const handleRejected = (state, payload) => {
26 | state.isLoading = false;
27 | state.error = payload.error;
28 | };
29 |
30 | const getStatistics = createSlice({
31 | name: 'statistics',
32 | initialState: contactsInitialValue,
33 | extraReducers: builder => {
34 | builder.addCase(
35 | getVideoCountAndBurnedCaloriesStatistics.pending,
36 | handlePending,
37 | );
38 | builder.addCase(
39 | getVideoCountAndBurnedCaloriesStatistics.fulfilled,
40 | (state, { payload }) => {
41 | handleFullfield(state, payload);
42 | state.allExercises = payload.AllExercises;
43 | state.allUsers = payload.AllUsers;
44 | state.usersBurnedCalories = formatNumber(payload.usersBurnedCalories);
45 | state.usersTimeTraining = payload.usersTimeTraining;
46 | state.usersTraining = payload.usersTraining;
47 | },
48 | );
49 | builder.addCase(
50 | getVideoCountAndBurnedCaloriesStatistics.rejected,
51 | handleRejected,
52 | );
53 | },
54 | });
55 |
56 | export const statisticsReducer = getStatistics.reducer;
57 |
--------------------------------------------------------------------------------
/src/redux/diary/operations.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { toast } from 'react-toastify';
4 |
5 | axios.defaults.baseURL = 'https://power-pulse-rest-api.onrender.com';
6 |
7 | export const getDiaryList = createAsyncThunk(
8 | 'getDiaryList',
9 | async (date, { rejectWithValue }) => {
10 | try {
11 | const { data } = await axios.get(`/api/diary?date=${date}`);
12 | return data;
13 | } catch (error) {
14 | toast.error('Oops... Something went wrong! Try again!');
15 | return rejectWithValue('Oops... Something went wrong!');
16 | }
17 | },
18 | );
19 |
20 | export const deleteProduct = createAsyncThunk(
21 | 'deleteProduct',
22 | async (productDetails, { rejectWithValue }) => {
23 | try {
24 | const { productId, date, calories, time } = productDetails;
25 | await axios.delete(
26 | `/api/diary/delete-product?date=${date}&productId=${productId}`,
27 | );
28 | return { productId, calories, time };
29 | } catch (error) {
30 | toast.error('Oops... Something went wrong! Try again!');
31 | return rejectWithValue('Oops... Something went wrong!');
32 | }
33 | },
34 | );
35 |
36 | export const deleteExercise = createAsyncThunk(
37 | 'deleteExercise',
38 | async (exerciseDetails, { rejectWithValue }) => {
39 | try {
40 | const { exerciseId, date, calories, time } = exerciseDetails;
41 | await axios.delete(
42 | `/api/diary/delete-exercise?date=${date}&exerciseId=${exerciseId}`,
43 | );
44 | return { exerciseId, calories, time };
45 | } catch (error) {
46 | toast.error('Oops... Something went wrong! Try again!');
47 | return rejectWithValue('Oops... Something went wrong!');
48 | }
49 | },
50 | );
51 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import CustomNavLink from '../../components/CustomNavLink/CustomNavLink';
3 | import MainTitle from '../../components/MainTitle/MainTitle';
4 | import ParamsBlockCard from '../../components/ParamsBlockСard/ParamsBlockCard';
5 | import { LinkList, Wrapper, WrapperDesktop } from './Home.styled';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { getVideoCountAndBurnedCaloriesStatistics } from '../../redux/statistic/operations';
8 | import {
9 | getAllExercises,
10 | getUsersBurnedCalories,
11 | } from '../../redux/statistic/selectors';
12 | import formatNumber from '../../utils/formatNumberStatistics';
13 |
14 | const Home = () => {
15 | const dispatch = useDispatch();
16 |
17 | const videoExercisesCount = useSelector(getAllExercises);
18 | const allBurnedCalories = useSelector(getUsersBurnedCalories);
19 |
20 | useEffect(() => {
21 | dispatch(getVideoCountAndBurnedCaloriesStatistics());
22 | }, [dispatch]);
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Home;
56 |
--------------------------------------------------------------------------------
/src/components/headersComp/UserNav/UserNav.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from 'react-redux';
2 | import { handleLogout } from '../../../utils';
3 | import CustomNavLink from '../../CustomNavLink/CustomNavLink';
4 | import {
5 | WrapUserNav,
6 | Button,
7 | Svg,
8 | SvgUser,
9 | ButtonWrap,
10 | Span,
11 | UserAvatar,
12 | } from './UserNav.styled';
13 | import { NavLink } from 'react-router-dom';
14 |
15 | import sprite from '../../../assets/sprite.svg';
16 | import { selectUser } from '../../../redux/auth/selectors';
17 |
18 | export const UserNav = () => {
19 | const dispatch = useDispatch();
20 | const { avatarURL } = useSelector(selectUser);
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {avatarURL ? (
34 |
35 |
36 |
37 | ) : (
38 |
39 |
40 |
41 |
42 |
43 | )}
44 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default UserNav;
60 |
--------------------------------------------------------------------------------
/src/pages/Diary/Diary.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const DiaryWrapper = styled.div`
5 | position: relative;
6 |
7 | padding: 0 20px 80px 20px;
8 | ${mq.tablet} {
9 | padding: 0 32px 64px 32px;
10 | }
11 | ${mq.desktop} {
12 | padding: 0 96px 68px 96px;
13 | }
14 | `;
15 |
16 | export const DiaryPageContainer = styled.div`
17 | width: auto;
18 | display: flex;
19 | flex-direction: column;
20 |
21 | ${mq.desktop} {
22 | flex-direction: row;
23 | justify-content: space-between;
24 | }
25 | `;
26 |
27 | export const CalendarContainer = styled.div`
28 | display: flex;
29 | position: absolute;
30 | align-items: center;
31 | z-index: 667;
32 | right: 20px;
33 | top: 4px;
34 |
35 | ${mq.tablet} {
36 | right: 32px;
37 | top: -10px;
38 | }
39 |
40 | ${mq.desktop} {
41 | right: 96px;
42 | }
43 | `;
44 |
45 | export const CalendarBtn = styled.button`
46 | height: 16px;
47 | padding: 0;
48 | margin-left: 40px;
49 |
50 | border: none;
51 | background-color: inherit;
52 |
53 | &:last-child {
54 | margin-left: 6px;
55 | }
56 |
57 | &:hover > svg {
58 | fill: ${colors.orange};
59 | }
60 | `;
61 |
62 | export const CalendarBtnIcon = styled.svg`
63 | width: 16px;
64 | height: 16px;
65 |
66 | transition: fill 0.3s;
67 |
68 | fill: ${colors.white};
69 | `;
70 |
71 | export const CustomDivForCards = styled.div`
72 | width: auto;
73 |
74 | ${mq.tablet} {
75 | margin-top: 64px;
76 | order: 1;
77 | }
78 |
79 | ${mq.desktop} {
80 | margin-top: 0;
81 | }
82 | `;
83 |
84 | export const CustomDivForTables = styled.div`
85 | width: auto;
86 |
87 | ${mq.tablet} {
88 | width: 704px;
89 | }
90 |
91 | ${mq.desktop} {
92 | width: 826px;
93 | }
94 | `;
95 |
--------------------------------------------------------------------------------
/src/components/Timer/Timer.jsx:
--------------------------------------------------------------------------------
1 | import { CountdownCircleTimer } from 'react-countdown-circle-timer';
2 | import PropTypes from 'prop-types';
3 |
4 | import sprite from '../../assets/sprite.svg';
5 | import {
6 | TimerTitle,
7 | FlexContainer,
8 | PauseButton,
9 | Svg,
10 | BurntCaloryLabel,
11 | BurntCaloryInfo,
12 | } from './Timer.styled';
13 |
14 | export default function Timer({
15 | writeTime,
16 | isPlaying,
17 | calory,
18 | startExercise,
19 | stopExercise,
20 | }) {
21 | return (
22 | <>
23 |
24 | Timer
25 |
33 | {writeTime}
34 |
35 |
36 |
37 |
38 | {isPlaying ? (
39 |
40 |
41 |
42 |
43 |
44 | ) : (
45 |
46 |
47 |
48 |
49 |
50 | )}
51 |
52 | Burned calories:
53 | {calory}
54 |
55 |
56 | >
57 | );
58 | }
59 |
60 | Timer.propTypes = {
61 | calory: PropTypes.number.isRequired,
62 | writeTime: PropTypes.func.isRequired,
63 | startExercise: PropTypes.func.isRequired,
64 | stopExercise: PropTypes.func.isRequired,
65 | isPlaying: PropTypes.bool.isRequired,
66 | };
67 |
--------------------------------------------------------------------------------
/src/components/AuthForm/AuthForm.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 | import { Form, Field } from 'formik';
4 |
5 | export const FormContainer = styled(Form)`
6 | margin-top: 28px;
7 |
8 | ${mq.tablet} {
9 | margin-top: 32px;
10 | width: 364px;
11 | }
12 | `;
13 |
14 | export const InputContainer = styled.div`
15 | display: flex;
16 | flex-direction: column;
17 | align-items: flex-start;
18 | gap: 32px;
19 | margin-bottom: 32px;
20 |
21 | ${mq.mobile} {
22 | gap: 33px;
23 | margin-bottom: 34px;
24 | }
25 |
26 | ${mq.tablet} {
27 | gap: 34px;
28 | margin-bottom: 54px;
29 | }
30 | `;
31 |
32 | export const InputWrapper = styled.div`
33 | position: relative;
34 | width: 100%;
35 | `;
36 |
37 | export const TextInput = styled(Field)`
38 | height: 42px;
39 | width: 100%;
40 | ${mq.tablet} {
41 | height: 48px;
42 | }
43 | padding-left: 14px;
44 | padding-right: 14px;
45 |
46 | border-radius: 12px;
47 | border: 1px solid ${colors.textWhite03};
48 |
49 | font-size: 16px;
50 | font-weight: 400;
51 | line-height: 150%;
52 |
53 | outline: none;
54 |
55 | color: ${colors.textWhite06};
56 | background-color: transparent;
57 | &:focus-visible {
58 | border: 1px solid ${colors.orange};
59 | }
60 |
61 | &[data-touch=true]{
62 | border-color: ${colors.textSuccess};
63 | }
64 | `;
65 |
66 | export const Warning = styled.div`
67 | position: absolute;
68 | ${mq.smallMobile} {
69 | bottom: -28px;
70 | }
71 |
72 | ${mq.Mobile} {
73 | bottom: -29px;
74 | }
75 |
76 | ${mq.tablet} {
77 | bottom: -30px;
78 | }
79 | `;
80 |
81 | export const Error = styled.div`
82 | display: flex;
83 | align-items: center;
84 | justify-content: center;
85 | gap: 4px;
86 | font-size: 12px;
87 | color: ${colors.textError};
88 | `;
89 |
--------------------------------------------------------------------------------
/src/components/ParamsBtn/ParamsBtn.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { BtnNav, Svg } from './ParamsBtn.styled';
3 | import Notiflix from 'notiflix';
4 |
5 | const ParamsBtn = ({ setSteps, type, step, values }) => {
6 | if (type === 'next') {
7 | return (
8 | {
11 | if (
12 | step === 2 &&
13 | values.height !== '' &&
14 | values.currentWeight !== '' &&
15 | values.desiredWeight !== ''
16 | ) {
17 | setSteps(step);
18 | return;
19 | }
20 |
21 | if (
22 | step === 3 &&
23 | values.blood !== '' &&
24 | values.sex !== '' &&
25 | values.levelActivity !== ''
26 | ) {
27 | setSteps(step);
28 | return;
29 | }
30 |
31 | Notiflix.Notify.warning('pls fill all fields');
32 | }}
33 | >
34 | Next
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | if (type === 'back') {
43 | return (
44 | {
47 | setSteps(step);
48 | }}
49 | >
50 |
51 |
52 |
53 | Back
54 |
55 | );
56 | }
57 | };
58 |
59 | ParamsBtn.propTypes = {
60 | setSteps: PropTypes.func.isRequired,
61 | type: PropTypes.string,
62 | step: PropTypes.number.isRequired,
63 | validate: PropTypes.func,
64 | values: PropTypes.object,
65 | };
66 |
67 | export default ParamsBtn;
68 |
--------------------------------------------------------------------------------
/src/components/DailyStatsCards/DailyStatsCards.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const CardWrap = styled.li`
5 | box-sizing: border-box;
6 | border: 1px solid;
7 |
8 | border-color: ${props => {
9 | if (props.border === 'green') {
10 | return colors.green;
11 | }
12 | if (props.border === 'red') {
13 | return colors.red;
14 | }
15 | return colors.textWhite03;
16 | }};
17 |
18 | background-color: ${props => {
19 | return props.fill === 'true' ? colors.orange : colors.textWhite005;
20 | }};
21 |
22 | height: 96px;
23 | border-radius: 12px;
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: space-between;
27 |
28 | height: 96px;
29 | padding: 10px;
30 | ${mq.mobile} {
31 | padding: 14px;
32 | }
33 | ${mq.tablet} {
34 | padding: 18px;
35 | height: 116px;
36 | }
37 | ${mq.desktop} {
38 | padding: 14px 18px;
39 | }
40 | `;
41 |
42 | export const KeyWrap = styled.div`
43 | display: flex;
44 | gap: 3px;
45 | ${mq.mobile} {
46 | gap: 8px;
47 | }
48 | `;
49 |
50 | export const Svg = styled.svg`
51 | width: 20px;
52 | height: 20px;
53 |
54 | fill: ${colors.orangeSecondary};
55 | `;
56 |
57 | export const Label = styled.span`
58 | color: ${props => {
59 | return props.fill === 'true' ? colors.textWhite08 : colors.textWhite05;
60 | }};
61 |
62 | font-family: Roboto;
63 | font-size: 12px;
64 | font-style: normal;
65 | font-weight: 400;
66 | line-height: ${16 / 12};
67 | ${mq.tablet} {
68 | line-height: ${18 / 12};
69 | }
70 | `;
71 |
72 | export const KeyValue = styled.p`
73 | color: ${colors.white};
74 | margin: 0;
75 |
76 | font-family: Roboto;
77 | font-size: 18px;
78 | font-style: normal;
79 | font-weight: 700;
80 | line-height: ${18 / 20};
81 |
82 | ${mq.tablet} {
83 | font-size: 24px;
84 | line-height: ${32 / 24};
85 | }
86 | `;
87 |
--------------------------------------------------------------------------------
/src/components/DayDiaryProductsOrExercises/DayDiaryProductsOrExercises.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import { colors, mq } from '../../utils';
5 |
6 | export const DayDiaryContainer = styled.div`
7 | overflow: auto;
8 |
9 | position: relative;
10 | box-sizing: border-box;
11 | display: block;
12 | width: 100%;
13 | height: 335px;
14 | padding: 15px;
15 | align-items: flex-start;
16 | margin-bottom: ${props => props.marginBottom}px;
17 |
18 | font-size: 14px;
19 | line-height: 1.29;
20 |
21 | border-radius: 12px;
22 | border: 1px solid ${colors.textWhite02};
23 | background: rgba(239, 237, 232, 0.05);
24 |
25 | ${mq.tablet} {
26 | width: auto;
27 | height: 234px;
28 | }
29 |
30 | ${mq.desktop} {
31 | width: auto;
32 | }
33 | `;
34 |
35 | export const DayDiarySubDiv = styled.div`
36 | display: flex;
37 | justify-content: space-between;
38 | `;
39 |
40 | export const DayDiarySubTitle = styled.p`
41 | margin: 0;
42 |
43 | font-size: 14px;
44 | line-height: 1.29;
45 |
46 | color: ${colors.textWhite05};
47 | `;
48 |
49 | export const AddLink = styled(NavLink)`
50 | display: flex;
51 | align-items: center;
52 |
53 | font-size: 16px;
54 | line-height: 1.5;
55 |
56 | color: ${colors.orange};
57 |
58 | transition:
59 | scale 250ms ease-in-out,
60 | color 250ms ease-in-out;
61 |
62 | &:hover {
63 | scale: 1.1;
64 | color: ${colors.orangeSecondary};
65 |
66 | svg {
67 | stroke: ${colors.orangeSecondary};
68 | }
69 | }
70 | `;
71 |
72 | export const DayNoContentText = styled.p`
73 | position: absolute;
74 | top: 50%;
75 | left: 50%;
76 | transform: translate(-50%, -50%);
77 |
78 | color: ${colors.textWhite05};
79 | `;
80 |
81 | export const ArrowRight = styled.svg`
82 | width: 20px;
83 | height: 20px;
84 | margin-left: 8px;
85 |
86 | stroke: ${colors.orange};
87 |
88 | transition: stroke 250ms ease-in-out;
89 | `;
90 |
--------------------------------------------------------------------------------
/src/components/ParamsBar/ParamsBar.jsx:
--------------------------------------------------------------------------------
1 | import { ProgressBar, BarItem } from './ParamsBar.styled';
2 | import PropTypes from 'prop-types';
3 | import { colors as palette } from '../../utils';
4 |
5 | const ParamsBar = ({ steps, setSteps }) => {
6 | const Btn = ({ steps, step, setSteps, children }) => {
7 | return (
8 |
24 | );
25 | };
26 |
27 | return (
28 |
29 |
30 | = 1 && `${palette.orangeSecondary}`,
33 | boxShadow: steps >= 1 && '0px 1px 10px 0px rgba(230, 83, 60, 0.8)',
34 | }}
35 | >
36 |
37 |
38 |
39 | = 2 && steps > 1 && `${palette.orangeSecondary}`,
42 | boxShadow:
43 | steps >= 2 &&
44 | steps > 1 &&
45 | '0px 1px 10px 0px rgba(230, 83, 60, 0.8)',
46 | }}
47 | >
48 |
49 |
50 |
56 |
57 |
58 | );
59 | };
60 |
61 | ParamsBar.propTypes = {
62 | steps: PropTypes.number.isRequired,
63 | step: PropTypes.number,
64 | setSteps: PropTypes.func.isRequired,
65 | children: PropTypes.any,
66 | };
67 |
68 | export default ParamsBar;
69 |
--------------------------------------------------------------------------------
/src/components/AddProductForm/AddProductForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import {
5 | AddButton,
6 | ButtonContainer,
7 | Calories,
8 | CloseButton,
9 | InputContainer,
10 | InputQuantity,
11 | InputTitle,
12 | TitleCalories,
13 | Label,
14 | InputWrapper,
15 | } from './AddProductForm.styled';
16 | import formatDate from '../../utils/formatDate';
17 |
18 | function AddProductForm({ data, closeModal, addProduct }) {
19 | const [quantity, setQuantity] = useState('');
20 |
21 | const amount = Math.round((quantity * data.calories) / 100);
22 | const date = formatDate(new Date());
23 |
24 | return (
25 |
65 | );
66 | }
67 |
68 | AddProductForm.propTypes = {
69 | data: PropTypes.object,
70 | closeModal: PropTypes.func,
71 | addProduct: PropTypes.func,
72 | };
73 |
74 | export default AddProductForm;
75 |
--------------------------------------------------------------------------------
/src/components/ProductOrExerciseModal/ProductOrExerciseModal.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../utils/colorVeriables';
3 | import { NavLink } from 'react-router-dom';
4 |
5 | export const ContentWrap = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | width: 190px;
9 | `;
10 |
11 | export const Img = styled.img`
12 | width: 120px;
13 | height: 97px;
14 | margin: 0 auto;
15 | margin-bottom: 19px;
16 | `;
17 |
18 | export const WellDone = styled.strong`
19 | margin: 0 auto;
20 | margin-bottom: 16px;
21 |
22 | color: ${color.white};
23 | font-family: Roboto;
24 | font-size: 24px;
25 | font-style: normal;
26 | font-weight: 700;
27 | line-height: ${32 / 24};
28 | `;
29 |
30 | export const DataList = styled.ul`
31 | margin: 0 auto;
32 | margin-bottom: 24px;
33 | display: flex;
34 | flex-direction: column;
35 | gap: 4px;
36 | `;
37 |
38 | export const Key = styled.p`
39 | color: ${color.textWhite03};
40 | font-family: Roboto;
41 | font-size: 14px;
42 | font-style: normal;
43 | font-weight: 400;
44 | line-height: ${18 / 14};
45 | display: flex;
46 | align-items: center;
47 | `;
48 |
49 | export const Value = styled.span`
50 | color: ${color.orange};
51 | font-family: Roboto;
52 | font-size: 14px;
53 | font-style: normal;
54 | font-weight: 400;
55 | line-height: ${14 / 18};
56 | margin-left: 5px;
57 | `;
58 |
59 | export const ToDiary = styled(NavLink)`
60 | margin: 0 auto;
61 | margin-top: 16px;
62 |
63 | display: flex;
64 | align-items: center;
65 | justify-content: baseline;
66 |
67 | gap: 8px;
68 |
69 | color: rgba(239, 237, 232, 0.3);
70 | font-family: Roboto;
71 | font-size: 14px;
72 | font-style: normal;
73 | font-weight: 400;
74 | line-height: 18px;
75 | transition:
76 | scale 200ms cubic-bezier(0.4, 0, 0.2, 1),
77 | color 200ms cubic-bezier(0.4, 0, 0.2, 1);
78 |
79 | &:focus,
80 | &:hover {
81 | color: ${color.orange};
82 | scale: 1.1;
83 | }
84 | `;
85 |
86 | export const Svg = styled.svg`
87 | stroke: currentColor;
88 | width: 16px;
89 | height: 16px;
90 | fill: ${color.textWhite04};
91 | scale: 1;
92 |
93 |
94 | `;
95 |
--------------------------------------------------------------------------------
/src/pages/SignIn/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from 'react-redux';
2 | import Title from '../../components/Title/Title';
3 | import SubTitle from '../../components/SubTitle/SubTitle';
4 | import AuthForm from '../../components/AuthForm/AuthForm';
5 | import BtnSubtitle from '../../components/BtnSubtitle/BtnSubtitle';
6 | import { Wrapper, WrapperDesktop } from '../Home/Home.styled';
7 | import ParamsBlockCard from '../../components/ParamsBlockСard';
8 | import { logInUser } from '../../redux/auth/operation';
9 | import { mg } from '../../utils';
10 | import {
11 | getAllExercises,
12 | getUsersBurnedCalories,
13 | } from '../../redux/statistic/selectors';
14 | import { useEffect } from 'react';
15 | import { getVideoCountAndBurnedCaloriesStatistics } from '../../redux/statistic/operations';
16 |
17 | const SignIn = () => {
18 | const dispatch = useDispatch();
19 |
20 | const videoExercisesCount = useSelector(getAllExercises);
21 | const allBurnedCalories = useSelector(getUsersBurnedCalories);
22 |
23 | useEffect(() => {
24 | dispatch(getVideoCountAndBurnedCaloriesStatistics());
25 | }, [dispatch]);
26 |
27 | const logIn = (user, { resetForm }) => {
28 | dispatch(logInUser(user));
29 | resetForm();
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
42 |
43 |
48 |
49 |
54 |
55 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default SignIn;
67 |
--------------------------------------------------------------------------------
/src/components/Calendar/Calendar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { forwardRef } from 'react';
3 | import DatePicker from 'react-datepicker';
4 | import 'react-datepicker/dist/react-datepicker-cssmodules.css';
5 | import { parseISO } from 'date-fns';
6 | import { Icon, Ipt, Label, GlobalStyles } from './Calendar.styled';
7 | import sprite from '../../assets/sprite.svg';
8 | import { Global } from '@emotion/react';
9 |
10 | export default function Calendar({
11 | name,
12 | value,
13 | onChange,
14 | maxDate,
15 | minDate,
16 | showYearDropdown,
17 | dateFormat,
18 | withoutВorder,
19 | customInput,
20 | }) {
21 | const ExampleCustomInput = forwardRef((dd, ref) => {
22 | const { value, onClick } = dd;
23 | return (
24 |
35 | );
36 | });
37 |
38 | ExampleCustomInput.displayName = 'Label';
39 |
40 | return (
41 | <>
42 | {
46 | onChange(name, date);
47 | }}
48 | maxDate={maxDate}
49 | minDate={minDate}
50 | yearDropdownItemNumber={40}
51 | customInput={customInput || }
52 | scrollableYearDropdown
53 | dateFormat={dateFormat || 'dd.MM.yyyy'}
54 | showYearDropdown={showYearDropdown}
55 | />
56 |
57 | >
58 | );
59 | }
60 |
61 | Calendar.propTypes = {
62 | onClick: PropTypes.func,
63 | name: PropTypes.string.isRequired,
64 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)])
65 | .isRequired,
66 | onChange: PropTypes.func.isRequired,
67 | maxDate: PropTypes.instanceOf(Date),
68 | minDate: PropTypes.instanceOf(Date),
69 | showYearDropdown: PropTypes.bool,
70 | dateFormat: PropTypes.string,
71 | withoutВorder: PropTypes.bool,
72 | customInput: PropTypes.object,
73 | };
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_vite",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "format": "prettier --write ./src"
12 | },
13 | "dependencies": {
14 | "@emotion/react": "^11.11.1",
15 | "@emotion/styled": "^11.11.0",
16 | "@mui/icons-material": "^5.14.9",
17 | "@mui/material": "^5.14.9",
18 | "@mui/x-date-pickers": "^6.14.0",
19 | "@reduxjs/toolkit": "^1.9.5",
20 | "@table-library/react-table-library": "^4.1.7",
21 | "animate.css": "^4.1.1",
22 | "axios": "^1.5.0",
23 | "date-fns": "^2.30.0",
24 | "dayjs": "^1.11.9",
25 | "formik": "^2.4.4",
26 | "lodash.debounce": "^4.0.8",
27 | "modern-normalize": "^2.0.0",
28 | "notiflix": "^3.2.6",
29 | "overlayscrollbars-react": "^0.5.2",
30 | "prop-types": "^15.8.1",
31 | "react": "^18.2.0",
32 | "react-countdown-circle-timer": "^3.2.1",
33 | "react-datepicker": "^4.18.0",
34 | "react-dom": "^18.2.0",
35 | "react-infinite-scroller": "^1.2.6",
36 | "react-loader-spinner": "^5.4.5",
37 | "react-redux": "^8.1.2",
38 | "react-responsive": "^9.0.2",
39 | "react-router-dom": "^6.15.0",
40 | "react-table": "^7.8.0",
41 | "react-toastify": "^9.1.3",
42 | "redux-persist": "^6.0.0",
43 | "styled-components": "^6.0.7",
44 | "swiper": "^10.2.0",
45 | "yup": "^1.2.0"
46 | },
47 | "devDependencies": {
48 | "@swc/cli": "^0.1.62",
49 | "@swc/core": "^1.3.87",
50 | "@types/react": "^18.2.15",
51 | "@types/react-dom": "^18.2.7",
52 | "@vitejs/plugin-react": "^4.0.3",
53 | "@vitejs/plugin-react-swc": "^3.3.2",
54 | "eslint": "^8.45.0",
55 | "eslint-config-prettier": "^9.0.0",
56 | "eslint-plugin-react": "^7.32.2",
57 | "eslint-plugin-react-hooks": "^4.6.0",
58 | "eslint-plugin-react-refresh": "^0.4.3",
59 | "husky": "^8.0.3",
60 | "lint-staged": "^14.0.1",
61 | "prettier": "3.0.3",
62 | "vite": "^4.4.5"
63 | },
64 | "lint-staged": {
65 | "*.{js,jsx}": "eslint --cache --fix",
66 | "*./src": "prettier --write"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/redux/productsFilter/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { fetchProducts, getCategories, fetchMoreProducts } from './operations';
3 |
4 | export const productsSlice = createSlice({
5 | name: 'products',
6 | initialState: {
7 | products: [],
8 | categories: [],
9 | error: null,
10 | isLoading: false,
11 | searchParams: '',
12 | hasMore: false,
13 | },
14 | reducers: {
15 | addSearchParams: {
16 | reducer(state, action) {
17 | state.searchParams = action.payload;
18 | },
19 | },
20 | },
21 | extraReducers: builder => {
22 | builder.addCase(fetchProducts.fulfilled, (state, action) => {
23 | state.products = action.payload;
24 | state.error = null;
25 | state.isLoading = false;
26 | state.hasMore = action.payload.length < 20 ? false : true;
27 | });
28 | builder.addCase(fetchProducts.pending, (state, action) => {
29 | state.isLoading = true;
30 | });
31 | builder.addCase(fetchProducts.rejected, (state, action) => {
32 | state.error = action.payload;
33 | state.isLoading = false;
34 | });
35 | builder.addCase(getCategories.fulfilled, (state, action) => {
36 | state.categories = action.payload;
37 | state.error = null;
38 | state.isLoading = false;
39 | });
40 | builder.addCase(getCategories.pending, (state, action) => {
41 | state.isLoading = true;
42 | });
43 | builder.addCase(getCategories.rejected, (state, action) => {
44 | state.error = action.payload;
45 | state.isLoading = false;
46 | });
47 | builder.addCase(fetchMoreProducts.fulfilled, (state, action) => {
48 | state.products = [...state.products, ...action.payload];
49 | state.error = null;
50 | state.isLoading = false;
51 | state.hasMore = action.payload.length < 20 ? false : true;
52 | });
53 | builder.addCase(fetchMoreProducts.pending, (state, action) => {
54 | state.isLoading = true;
55 | });
56 | builder.addCase(fetchMoreProducts.rejected, (state, action) => {
57 | state.error = action.payload;
58 | state.isLoading = false;
59 | });
60 | },
61 | });
62 |
63 | export default productsSlice.reducer;
64 |
65 | export const { addSearchParams } = productsSlice.actions;
66 |
--------------------------------------------------------------------------------
/src/components/ProductOrExerciseModal/ProductOrExerciseModal.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import BtnSubmit from '../BtnSubmit/BtnSubmit';
3 | import {
4 | ContentWrap,
5 | Img,
6 | WellDone,
7 | Key,
8 | Value,
9 | ToDiary,
10 | Svg,
11 | DataList,
12 | } from './ProductOrExerciseModal.styled';
13 | import { nanoid } from '@reduxjs/toolkit';
14 | import foodIcon from "../../assets/images/foodIcon.png";
15 | import thumbUp from "../../assets/images/thumbUp.png";
16 | import sprite from '../../assets/sprite.svg';
17 |
18 | function ProductOrExerciseModal({ modalType, data, btnNext }) {
19 | const transformKey = key => {
20 | if (key === 'time') {
21 | return 'Your time: ';
22 | }
23 | if (key === 'burnedCalories') {
24 | return 'Burned calories: ';
25 | }
26 | if (key === 'calories') {
27 | return 'Calories: ';
28 | }
29 | };
30 |
31 | const mappedData = data => {
32 | const keys = Object.keys(data);
33 | const renderKeys = keys.map(key => {
34 |
35 | return (
36 |
37 |
38 | {`${transformKey(key)} `}
39 | {key === "time" && `${data[key]} seconds` } {key === "burnedCalories" && `${data[key]}`} {key === "calories" && `${data[key]}`}
40 |
41 |
42 | );
43 | });
44 | return renderKeys;
45 | };
46 |
47 | return (
48 |
49 |
53 | Well Done
54 |
55 |
56 |
57 |
61 |
62 |
63 | To the diary
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | ProductOrExerciseModal.propTypes = {
73 | modalType: PropTypes.oneOf(['product', 'exercise']),
74 | data: PropTypes.object.isRequired,
75 | btnNext: PropTypes.func,
76 | };
77 |
78 | export default ProductOrExerciseModal;
79 |
--------------------------------------------------------------------------------
/src/pages/SignUp/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import Title from '../../components/Title/Title';
2 | import SubTitle from '../../components/SubTitle/SubTitle';
3 | import AuthForm from '../../components/AuthForm';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { authUser } from '../../redux/auth/operation';
6 | import BtnSubtitle from '../../components/BtnSubtitle/BtnSubtitle';
7 | import { Wrapper, WrapperDesktop } from '../Home/Home.styled';
8 | import ParamsBlockCard from '../../components/ParamsBlockСard/ParamsBlockCard';
9 | import { mg } from '../../utils';
10 | import {
11 | getAllExercises,
12 | getUsersBurnedCalories,
13 | } from '../../redux/statistic/selectors';
14 | import { useEffect } from 'react';
15 | import { getVideoCountAndBurnedCaloriesStatistics } from '../../redux/statistic/operations';
16 |
17 | const SignUp = () => {
18 | const dispatch = useDispatch();
19 |
20 | const videoExercisesCount = useSelector(getAllExercises);
21 | const allBurnedCalories = useSelector(getUsersBurnedCalories);
22 |
23 | useEffect(() => {
24 | dispatch(getVideoCountAndBurnedCaloriesStatistics());
25 | }, [dispatch]);
26 |
27 | const handleSubmit = (user, { resetForm }) => {
28 | dispatch(authUser(user));
29 | resetForm();
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
42 |
47 |
48 |
53 |
54 |
59 |
60 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default SignUp;
72 |
--------------------------------------------------------------------------------
/src/components/AddExerciseForm/AddExerciseForm.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const Container = styled.div`
5 | position: relative;
6 | z-index: 100;
7 | width: 335px;
8 | height: 100%;
9 | margin-left: auto;
10 | margin-right: auto;
11 | border: transparent;
12 |
13 | background-color: ${colors.modalBackground};
14 | ${mq.tablet} {
15 | width: 694px;
16 | display: flex;
17 | gap: 15px;
18 | }
19 | `;
20 |
21 | export const GifContainer = styled.div`
22 | width: 270px;
23 | height: 226px;
24 | overflow: hidden;
25 | border: 1px solid ${colors.textWhite02};
26 | border-radius: 12px;
27 | `;
28 |
29 | export const Img = styled.img`
30 | width: 270px;
31 | height: 226px;
32 | `;
33 |
34 | export const TimerContainer = styled.div`
35 | display: flex;
36 | align-items: center;
37 | justify-content: center;
38 | flex-direction: column;
39 | gap: 14px;
40 | margin-bottom: 40px;
41 | ${mq.tablet} {
42 | margin-bottom: 0;
43 | }
44 | `;
45 |
46 | export const InfoCardConteiner = styled.div`
47 | display: flex;
48 | flex-wrap: wrap;
49 | gap: 8px;
50 | margin-bottom: 24px;
51 | ${mq.tablet} {
52 | height: 226px;
53 | }
54 | `;
55 |
56 | export const InfoCard = styled.div`
57 | flex-basis: calc((100% - 8px) / 2);
58 |
59 | min-height: 62px;
60 | padding: 12px 18px;
61 | border-radius: 12px;
62 | border: 1px solid ${colors.textWhite02};
63 | background-color: ${colors.textWhite005};
64 | ${mq.tablet} {
65 | min-height: 68px;
66 | }
67 | `;
68 |
69 | export const CardTitle = styled.div`
70 | margin-bottom: 4px;
71 | font-size: 12px;
72 | line-height: 133%;
73 | color: ${colors.textWhite04};
74 | `;
75 |
76 | export const CardInfo = styled.div`
77 | font-size: 14px;
78 | font-weight: 700;
79 | line-height: 128.571%;
80 | color: ${colors.white};
81 | ${mq.tablet} {
82 | font-size: 16px;
83 | line-height: 150%;
84 | }
85 | `;
86 |
87 | export const ButtonWrapper = styled.div`
88 | ${mq.tablet} {
89 | position: absolute;
90 | bottom: 48px;
91 | right: 32px;
92 | }
93 | `;
94 |
95 | export const Watch = styled.span`
96 | font-size: 16px;
97 | line-height: 150%;
98 | color: ${colors.white};
99 | `;
100 |
--------------------------------------------------------------------------------
/src/assets/images/imgParamsForm.js:
--------------------------------------------------------------------------------
1 | import imgS1Dx1 from '../images/params-step1_desktop_1x.jpg';
2 | import imgS1Dx2 from '../images/params-step1_desktop_2x.jpg';
3 | import imgS1Dx3 from '../images/params-step1_desktop_3x.jpg';
4 |
5 | import imgS2Dx1 from '../images/params-step2_desktop_1x.jpg';
6 | import imgS2Dx2 from '../images/params-step2_desktop_2x.jpg';
7 | import imgS2Dx3 from '../images/params-step2_desktop_3x.jpg';
8 |
9 | import imgS3Dx1 from '../images/params-step3_desktop_1x.jpg';
10 | import imgS3Dx2 from '../images/params-step3_desktop_2x.jpg';
11 | import imgS3Dx3 from '../images/params-step3_desktop_3x.jpg';
12 |
13 | import imgS1Tx1 from '../images/params-step1_tablet_1x.jpg';
14 | import imgS1Tx2 from '../images/params-step1_tablet_2x.jpg';
15 | import imgS1Tx3 from '../images/params-step1_tablet_3x.jpg';
16 |
17 | import imgS2Tx1 from '../images/params-step2_tablet_1x.jpg';
18 | import imgS2Tx2 from '../images/params-step2_tablet_2x.jpg';
19 | import imgS2Tx3 from '../images/params-step2_tablet_3x.jpg';
20 |
21 | import imgS3Tx1 from '../images/params-step3_tablet_1x.jpg';
22 | import imgS3Tx2 from '../images/params-step3_tablet_2x.jpg';
23 | import imgS3Tx3 from '../images/params-step3_tablet_3x.jpg';
24 |
25 | import imgS1Mx1 from '../images/params-step1_mobile_1x.jpg';
26 | import imgS1Mx2 from '../images/params-step1_mobile_2x.jpg';
27 | import imgS1Mx3 from '../images/params-step1_mobile_3x.jpg';
28 |
29 | import imgS2Mx1 from '../images/params-step2_mobile_1x.jpg';
30 | import imgS2Mx2 from '../images/params-step2_mobile_2x.jpg';
31 | import imgS2Mx3 from '../images/params-step2_mobile_3x.jpg';
32 |
33 | import imgS3Mx1 from '../images/params-step3_mobile_1x.jpg';
34 | import imgS3Mx2 from '../images/params-step3_mobile_2x.jpg';
35 | import imgS3Mx3 from '../images/params-step3_mobile_3x.jpg';
36 |
37 | const imgPrmsForm = {
38 | desktop: {
39 | imgS1Dx1,
40 | imgS1Dx2,
41 | imgS1Dx3,
42 |
43 | imgS2Dx1,
44 | imgS2Dx2,
45 | imgS2Dx3,
46 |
47 | imgS3Dx1,
48 | imgS3Dx2,
49 | imgS3Dx3,
50 | },
51 |
52 | tablet: {
53 | imgS1Tx1,
54 | imgS1Tx2,
55 | imgS1Tx3,
56 |
57 | imgS2Tx1,
58 | imgS2Tx2,
59 | imgS2Tx3,
60 |
61 | imgS3Tx1,
62 | imgS3Tx2,
63 | imgS3Tx3,
64 | },
65 |
66 | mobile: {
67 | imgS1Mx1,
68 | imgS1Mx2,
69 | imgS1Mx3,
70 |
71 | imgS2Mx1,
72 | imgS2Mx2,
73 | imgS2Mx3,
74 |
75 | imgS3Mx1,
76 | imgS3Mx2,
77 | imgS3Mx3,
78 | },
79 | };
80 |
81 | export default imgPrmsForm;
82 |
--------------------------------------------------------------------------------
/src/components/ExercisesCategories/ExercisesCategories.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { fetchFilters } from '../../redux/exerciseFilters/operations';
4 | import { setStatusFilter } from '../../redux/exerciseFilters/slice';
5 | import { selectGetFilters } from '../../redux/exercises/selectors';
6 | import { changeStatusFilter } from '../../redux/exercises/slice';
7 | import { selectFilter } from '../../redux/exerciseFilters/selectors';
8 | import {
9 | CategoriesList,
10 | CategoriesListItem,
11 | CategoriesLink,
12 | } from './ExercisesCategories.styled';
13 |
14 | const ExercisesCategories = () => {
15 | let activeBtn = useSelector(selectFilter);
16 | const [activeCategory, setActiveCategory] = useState(activeBtn); // Используйте локальное состояние для хранения активной категории
17 | const filter = useSelector(selectGetFilters);
18 |
19 | const dispatch = useDispatch();
20 |
21 | useEffect(() => {
22 | dispatch(fetchFilters());
23 | }, [dispatch, filter]);
24 |
25 | const handleCategoryChange = event => {
26 | const currentCategory = event.target.childNodes[0].textContent;
27 |
28 | dispatch(changeStatusFilter(true));
29 |
30 | setActiveCategory(currentCategory); // Устанавливаем активную категорию
31 |
32 | dispatch(setStatusFilter(currentCategory));
33 | };
34 |
35 | return (
36 | <>
37 |
38 |
39 |
44 | Body parts
45 |
46 |
47 |
48 |
53 | Muscles
54 |
55 |
56 |
57 |
62 | Equipment
63 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
70 | export default ExercisesCategories;
71 |
--------------------------------------------------------------------------------
/src/components/DayDiaryProductsOrExercises/DayDiaryProductsOrExercises.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import {
4 | DayDiaryContainer,
5 | DayDiarySubTitle,
6 | AddLink,
7 | DayNoContentText,
8 | ArrowRight,
9 | DayDiarySubDiv,
10 | } from './DayDiaryProductsOrExercises.styled';
11 | import sprite from '../../assets/sprite.svg';
12 | import TableForDiary from '../TableForDiary/TableForDiary';
13 | import TableForDiaryOnMobile from '../TableForDiaryOnMobile/TableForDiaryOnMobile';
14 | import { useDispatch } from 'react-redux';
15 | import { deleteExercise, deleteProduct } from '../../redux/diary/operations';
16 |
17 | const DayDiaryProductsOrExercises = ({
18 | to,
19 | marginBottom,
20 | list,
21 | productTable,
22 | exerciseTable,
23 | date,
24 | }) => {
25 | const dispatch = useDispatch();
26 |
27 | const handleDelete = ({ date, id, calories, time }) => {
28 | if (productTable) {
29 | dispatch(deleteProduct({ productId: id, date, calories, time }));
30 | }
31 | if (exerciseTable) {
32 | dispatch(deleteExercise({ exerciseId: id, date, calories, time }));
33 | }
34 | };
35 |
36 | return (
37 |
38 |
39 |
40 | {productTable ? 'Products' : 'Exercises'}
41 |
42 |
43 | Add {productTable ? 'product' : 'exercise'}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {list.length !== 0 ? (
51 | <>
52 |
59 |
66 | >
67 | ) : (
68 |
69 | Not found {productTable ? 'products' : 'exercises'}
70 |
71 | )}
72 |
73 | );
74 | };
75 |
76 | DayDiaryProductsOrExercises.propTypes = {
77 | to: PropTypes.string,
78 | marginBottom: PropTypes.number,
79 | list: PropTypes.array,
80 | productTable: PropTypes.bool,
81 | exerciseTable: PropTypes.bool,
82 | date: PropTypes.string,
83 | };
84 |
85 | export default DayDiaryProductsOrExercises;
86 |
--------------------------------------------------------------------------------
/src/components/TableForDiaryOnMobile/TableForDiaryOnMobile.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 |
4 | export const CustomContainer = styled.div`
5 | overflow: auto;
6 | margin-top: 22px;
7 | height: 254px;
8 |
9 | ::-webkit-scrollbar {
10 | width: 6px;
11 | height: 90px;
12 | }
13 | ::-webkit-scrollbar-thumb {
14 | background-color: ${colors.textWhite01};
15 | border-radius: 12px;
16 | }
17 |
18 | ${mq.tablet} {
19 | display: none;
20 | }
21 | `;
22 |
23 | export const ContainerForTable = styled.div`
24 | padding-right: 14px;
25 | margin-bottom: 40px;
26 | height: auto;
27 |
28 | &:last-child {
29 | margin-bottom: 8px;
30 | }
31 | `;
32 |
33 | export const BottomContainer = styled.div`
34 | display: flex;
35 | height: 64px;
36 |
37 | & > div {
38 | &:nth-of-type(1) {
39 | width: 81px;
40 | margin-right: 16px;
41 | }
42 | }
43 |
44 | & > div {
45 | &:nth-of-type(2) {
46 | width: 80px;
47 | margin-right: 16px;
48 | }
49 | }
50 |
51 | & > div {
52 | &:nth-of-type(3) {
53 | width: 76px;
54 | margin-right: 8px;
55 |
56 | & > p {
57 | &::before {
58 | ${props =>
59 | props.before &&
60 | `
61 | content: '';
62 | width: 14px;
63 | height: 14px;
64 | border-radius: 10px;
65 | background: #419b09;
66 | margin-right: 8px;
67 | `}
68 | }
69 | }
70 | }
71 | }
72 |
73 | & > div {
74 | &:nth-of-type(4) {
75 | & > p {
76 | padding: 0;
77 | margin-top: 35px;
78 |
79 | border: none;
80 | }
81 | }
82 | }
83 | `;
84 |
85 | export const Cell = styled.div`
86 | font-size: 12px;
87 | line-height: 1.5;
88 |
89 | color: ${colors.orange};
90 | `;
91 |
92 | export const CellValue = styled.p`
93 | display: flex;
94 | align-items: center;
95 | margin-top: 8px;
96 | margin-bottom: 16px;
97 | padding: 10px 0 10px 14px;
98 |
99 | font-size: 14px;
100 | line-height: 1.29;
101 |
102 | border-radius: 12px;
103 | border: 1px solid ${colors.textWhite03};
104 |
105 | color: ${colors.white};
106 |
107 | &::before {
108 | ${props =>
109 | props.before &&
110 | `
111 | content: '';
112 | width: 14px;
113 | height: 14px;
114 | border-radius: 10px;
115 | background: ${props.colorBefore ? '#419B09' : '#E9101D'};
116 | margin-right: 8px;
117 | `}
118 | }
119 | `;
120 |
--------------------------------------------------------------------------------
/src/redux/exercises/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { addExercise, getExercises, getMoreExercises } from './operations';
3 |
4 | export const exercisesSlice = createSlice({
5 | name: 'exercise',
6 | initialState: {
7 | items: [],
8 | error: null,
9 | isLoading: false,
10 | getFilters: true,
11 | hasMore: false,
12 | searchParams: '',
13 | isTimerOn: false,
14 | },
15 |
16 | reducers: {
17 | changeStatusFilter: (state, action) => {
18 | state.getFilters = action.payload;
19 | },
20 | changeStatusTimer: (state, action) => {
21 | state.isTimerOn = action.payload;
22 | },
23 | addSearchExerciseParams: {
24 | reducer(state, action) {
25 | state.searchParams = action.payload;
26 | },
27 | },
28 | },
29 | extraReducers: builder => {
30 | builder.addCase(getExercises.fulfilled, (state, action) => {
31 | state.items = action.payload;
32 | state.getFilters = false;
33 | state.error = null;
34 | state.isLoading = false;
35 | state.hasMore = action.payload.length < 20 ? false : true;
36 | });
37 | builder.addCase(getExercises.rejected, (state, action) => {
38 | state.error = action.payload;
39 | state.isLoading = false;
40 | });
41 | builder.addCase(getExercises.pending, state => {
42 | state.isLoading = true;
43 | state.error = null;
44 | });
45 |
46 | builder.addCase(addExercise.fulfilled, state => {
47 | state.isLoading = false;
48 | state.error = null;
49 | });
50 | builder.addCase(addExercise.rejected, (state, action) => {
51 | state.error = action.payload;
52 | state.isLoading = false;
53 | });
54 | builder.addCase(addExercise.pending, state => {
55 | // state.isLoading = true;
56 | state.error = null;
57 | });
58 | builder.addCase(getMoreExercises.fulfilled, (state, action) => {
59 | state.items = [...state.items, ...action.payload];
60 | state.getFilters = false;
61 | state.error = null;
62 | state.isLoading = false;
63 | state.hasMore = action.payload.length < 20 ? false : true;
64 | });
65 | builder.addCase(getMoreExercises.rejected, (state, action) => {
66 | state.error = action.payload;
67 | state.isLoading = false;
68 | });
69 | builder.addCase(getMoreExercises.pending, state => {
70 | state.isLoading = true;
71 | state.isLoading = true;
72 | });
73 | },
74 | });
75 | export const {
76 | changeStatusFilter,
77 | changeStatusTimer,
78 | addSearchExerciseParams,
79 | } = exercisesSlice.actions;
80 |
81 | export default exercisesSlice.reducer;
82 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { imgForHome } from '../../assets/images';
4 | import { mq } from '../../utils';
5 |
6 | export const LinkList = styled.ul`
7 | display: flex;
8 | gap: 20px;
9 | `;
10 |
11 | export const Wrapper = styled.div`
12 | position: relative;
13 | height: 685px;
14 | background-image: url(${imgForHome.imgMx1});
15 | background-repeat: no-repeat;
16 | background-position: right bottom;
17 | background-size: 298px 571px;
18 | padding: 0 20px;
19 |
20 | @media (min-device-pixel-ratio: 2),
21 | (-webkit-min-device-pixel-ratio: 2),
22 | (min-resolution: 192dpi),
23 | (min-resolution: 2dppx) {
24 | & {
25 | background-image: url(${imgForHome.imgMx2});
26 | }
27 | }
28 |
29 | @media (min-device-pixel-ratio: 3),
30 | (-webkit-min-device-pixel-ratio: 3),
31 | (min-resolution: 288dpi),
32 | (min-resolution: 3dppx) {
33 | & {
34 | background-image: url(${imgForHome.imgMx3});
35 | }
36 | }
37 |
38 | ${mq.tablet} {
39 | height: 832px;
40 | background-image: url(${imgForHome.imgTx1});
41 | background-size: 437px 893px;
42 | padding: 0 32px;
43 |
44 | @media (min-device-pixel-ratio: 2),
45 | (-webkit-min-device-pixel-ratio: 2),
46 | (min-resolution: 192dpi),
47 | (min-resolution: 2dppx) {
48 | & {
49 | background-image: url(${imgForHome.imgTx2});
50 | }
51 | }
52 | @media (min-device-pixel-ratio: 3),
53 | (-webkit-min-device-pixel-ratio: 3),
54 | (min-resolution: 288dpi),
55 | (min-resolution: 3dppx) {
56 | & {
57 | background-image: url(${imgForHome.imgTx3});
58 | }
59 | }
60 | }
61 | ${mq.desktop} {
62 | height: 0;
63 | background: none;
64 | padding: 0 0 0 96px;
65 | }
66 | `;
67 |
68 | export const WrapperDesktop = styled.div`
69 | ${mq.desktop} {
70 | position: absolute;
71 | top: 0;
72 | right: 0;
73 | width: 670px;
74 | height: 800px;
75 | pointer-events: none;
76 |
77 | background-image: url(${imgForHome.imgDx1});
78 | background-size: 670px 800px;
79 |
80 | @media (min-device-pixel-ratio: 2),
81 | (-webkit-min-device-pixel-ratio: 2),
82 | (min-resolution: 192dpi),
83 | (min-resolution: 2dppx) {
84 | & {
85 | background-image: url(${imgForHome.imgDx2});
86 | }
87 | }
88 | @media (min-device-pixel-ratio: 3),
89 | (-webkit-min-device-pixel-ratio: 3),
90 | (min-resolution: 288dpi),
91 | (min-resolution: 3dppx) {
92 | & {
93 | background-image: url(${imgForHome.imgDx3});
94 | }
95 | }
96 | }
97 | `;
98 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'node_modules/modern-normalize/modern-normalize.css';
2 |
3 | @font-face {
4 | font-family: 'Roboto';
5 | font-weight: 400;
6 | src:
7 | url('assets/fonts/roboto-regular-webfont.woff2') format('woff2'),
8 | url('assets/fonts/roboto-regular-webfont.woff') format('woff');
9 | }
10 |
11 | @font-face {
12 | font-family: 'Roboto';
13 | font-weight: 500;
14 | src:
15 | url('assets/fonts/roboto-medium-webfont.woff2') format('woff2'),
16 | url('assets/fonts/roboto-medium-webfont.woff') format('woff');
17 | }
18 | @font-face {
19 | font-family: 'Roboto';
20 | font-weight: 700;
21 | src:
22 | url('assets/fonts/roboto-bold-webfont.woff2') format('woff2'),
23 | url('assets/fonts/roboto-bold-webfont.woff') format('woff');
24 | }
25 |
26 |
27 |
28 | body {
29 | background-color: rgb(0, 0, 0);
30 | background-size: auto 100%;
31 | background-position: center center;
32 | background-repeat: no-repeat;
33 | font-family: 'Roboto', sans-serif;
34 | font-weight: 400;
35 | font-style: normal;
36 | color: #111111;
37 | width: 100%;
38 | height: 100vh;
39 | margin: 0;
40 | }
41 |
42 | h1,
43 | h2,
44 | h3,
45 | h4,
46 | h5,
47 | h6,
48 | p {
49 | margin: 0;
50 | }
51 | a {
52 | text-decoration: none;
53 | }
54 | a:hover {
55 | cursor: pointer;
56 | }
57 | ul {
58 | list-style: none;
59 | padding: 0;
60 | margin: 0;
61 | }
62 | img {
63 | display: block;
64 | }
65 | button:hover {
66 | cursor: pointer;
67 | }
68 | button:disabled {
69 | cursor: not-allowed;
70 | }
71 |
72 | .css-9l3uo3 {
73 | color: #efede8;
74 | font-size: 14px;
75 | font-weight: 400;
76 | line-height: 128%;
77 | letter-spacing: normal;
78 |
79 | @media screen and (min-width: 768px) {
80 | font-size: 16px;
81 | }
82 | }
83 |
84 | .os-scrollbar-vertical {
85 | background-color: rgba(239, 237, 232, 0.1);
86 | border-radius: 12px;
87 | /* width: 8px; */
88 | --os-size: 8px;
89 | --os-track-border-radius: 12px;
90 | --os-handle-bg: #ef8964;
91 | --os-handle-bg-active: #ef8964;
92 | --os-handle-bg-hover: #ef8964;
93 | --os-handle-min-size: 33px;
94 | --os-handle-max-size: 153px;
95 | --os-handle-perpendicular-size: 8px;
96 | --os-handle-perpendicular-size-active: 8px;
97 | --os-handle-perpendicular-size-hover: 8px;
98 | }
99 |
100 | /* стили слайдера для пагинации */
101 |
102 | .pagination .swiper-pagination-bullet {
103 | width: 14px;
104 | height: 14px;
105 | background: #efede8;
106 | }
107 |
108 | .pagination .swiper-pagination-bullet-active {
109 | background: #e6533c;
110 | }
111 |
112 | .body-scroll-lock {
113 | overflow: hidden;
114 | }
--------------------------------------------------------------------------------
/src/components/ExercisesItemList/ExercisesItemList.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import ExercisesItem from '../ExercisesItem/ExercisesItem';
3 | import { ExercisesItemList } from './ExercisesItemList.styled';
4 | import { capitalizeWord } from '../../utils/capitalizeWord';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import {
7 | selectItems,
8 | selectFilter,
9 | } from '../../redux/exerciseFilters/selectors';
10 | import { getExercises } from '../../redux/exercises/operations';
11 |
12 | import { setCurrentTitle } from '../../redux/exerciseFilters/slice';
13 |
14 | import { Navigation, Pagination, Scrollbar, A11y } from 'swiper/modules';
15 |
16 | import { Swiper, SwiperSlide } from 'swiper/react';
17 | import { useMediaQuery } from 'react-responsive';
18 |
19 | import 'swiper/css';
20 | import 'swiper/css/navigation';
21 | import 'swiper/css/pagination';
22 | import 'swiper/css/scrollbar';
23 | import { nanoid } from '@reduxjs/toolkit';
24 | import { addSearchExerciseParams } from '../../redux/exercises/slice';
25 |
26 | const ExercisesList = () => {
27 | const dispatch = useDispatch();
28 |
29 | const handleGetExercises = (params, name) => {
30 | dispatch(getExercises(params));
31 | dispatch(addSearchExerciseParams(params));
32 | dispatch(setCurrentTitle(name));
33 | };
34 |
35 | let category = useSelector(selectFilter);
36 | let filters = useSelector(selectItems);
37 |
38 | const array = filters.filter(item => item.filter === category);
39 |
40 | const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 1439 });
41 |
42 | const chunkedFilters = [];
43 |
44 | if (isTablet) {
45 | for (let i = 0; i < array.length; i += 9) {
46 | chunkedFilters.push(array.slice(i, i + 9));
47 | }
48 | } else {
49 | for (let i = 0; i < array.length; i += 10) {
50 | chunkedFilters.push(array.slice(i, i + 10));
51 | }
52 | }
53 |
54 | return (
55 |
63 | {chunkedFilters.map(arr => (
64 |
65 |
66 | {arr.map(({ filter, name, imgURL, _id }) => (
67 |
74 | ))}
75 |
76 |
77 | ))}
78 |
79 | );
80 | };
81 |
82 | ExercisesList.propTypes = {
83 | bodyParts: PropTypes.arrayOf(
84 | PropTypes.shape({
85 | name: PropTypes.string.isRequired,
86 | }),
87 | ),
88 | };
89 |
90 | export default ExercisesList;
91 |
--------------------------------------------------------------------------------
/src/pages/Products/Products.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from 'react-redux';
2 | import { useEffect, useState } from 'react';
3 | import InfiniteScroll from 'react-infinite-scroller';
4 |
5 | import { FlexWrapper, ProductPageContainer } from './Products.styled';
6 | import ProductsOrExercisesContainer from '../../components/ProductOrExerciseContainer/ProductOrExerciseContainer';
7 | import Title from '../../components/Title/Title';
8 | import ProductsFilter from '../../components/ProductsFilter/ProductsFilter';
9 | import ScrollBar from '../../components/Scrollbar';
10 | import ProductsOrExercisesItem from '../../components/ProductsOrExercisesItem/ProductsOrExercisesItem';
11 | import EmptyProductList from '../../components/EmptyProductList/EmptyProductList';
12 | import Loader from '../../components/Lodaer/Loader';
13 |
14 | import { fetchMoreProducts } from '../../redux/productsFilter/operations';
15 | import {
16 | getIsLoading,
17 | getSearchParams,
18 | getProducts,
19 | getHasMore,
20 | } from '../../redux/productsFilter/selectors';
21 |
22 | const Products = () => {
23 | const [page, setPage] = useState(1);
24 |
25 | const hasMore = useSelector(getHasMore);
26 | const isLoadingMoreProducts = useSelector(getIsLoading);
27 | const products = useSelector(getProducts);
28 |
29 | const dispatch = useDispatch();
30 |
31 | const searchParams = useSelector(getSearchParams);
32 |
33 | useEffect(() => {
34 | setPage(1);
35 | }, [searchParams]);
36 |
37 | const onLoadMore = () => {
38 | if (page === 1) {
39 | setPage(prevPage => prevPage + 1);
40 | return;
41 | }
42 |
43 | const paginationParams = new URLSearchParams({
44 | page,
45 | limit: 20,
46 | }).toString();
47 | dispatch(fetchMoreProducts(`${searchParams}&${paginationParams}`));
48 | setPage(prevPage => prevPage + 1);
49 | };
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 | {products.length !== 0 ? (
59 |
60 | }
65 | useWindow={false}
66 | >
67 |
68 | {products.map(product => {
69 | return (
70 |
75 | );
76 | })}
77 |
78 |
79 |
80 | ) : (
81 |
82 | )}
83 |
84 |
85 | );
86 | };
87 |
88 | export default Products;
89 |
--------------------------------------------------------------------------------
/src/pages/Error/Error.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import imgDx2 from '../../assets/images/exercises_desk_2x.jpeg';
3 | import { colors, mq } from '../../utils';
4 |
5 | export const Container = styled.div`
6 | display: flex;
7 | position: relative;
8 | ${mq.mobile} {
9 | width: 375px;
10 | }
11 |
12 | ${mq.tablet} {
13 | width: 768px;
14 | }
15 |
16 | ${mq.desktop} {
17 | width: 1440px;
18 | }
19 | `;
20 |
21 | export const Content = styled.div`
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: flex-start;
25 | align-items: flex-start;
26 | height: 100vh;
27 |
28 | padding-left: 20px;
29 | padding-right: 20px;
30 | padding-bottom: 20px;
31 | background-color: ${colors.orange};
32 |
33 | ${mq.mobile} {
34 | width: 240px;
35 | }
36 |
37 | ${mq.tablet} {
38 | width: 420px;
39 | padding-left: 32px;
40 | padding-right: 32px;
41 | padding-bottom: 32px;
42 | height: 100%;
43 | }
44 |
45 | ${mq.desktop} {
46 | width: 670px;
47 | padding-left: 96px;
48 | padding-right: 96px;
49 | padding-bottom: 96px;
50 | height: 100vh;
51 | }
52 | `;
53 |
54 | export const Title = styled.h1`
55 | margin-bottom: 14px;
56 |
57 | color: ${colors.white};
58 | font-family: Roboto;
59 | font-size: 66px;
60 | font-style: normal;
61 | font-weight: 500;
62 | line-height: 1;
63 | letter-spacing: 0.66px;
64 |
65 | ${mq.mobile} {
66 | }
67 |
68 | ${mq.tablet} {
69 | font-size: 160px;
70 | margin-bottom: 28px;
71 | }
72 | `;
73 |
74 | export const Text = styled.p`
75 | margin-bottom: 28px;
76 |
77 | color: ${colors.white};
78 | font-family: Roboto;
79 | font-size: 14px;
80 | font-style: normal;
81 | font-weight: 400;
82 | line-height: 1.28;
83 |
84 | ${mq.mobile} {
85 | width: 200px;
86 | }
87 |
88 | ${mq.tablet} {
89 | width: 356px;
90 | font-size: 16px;
91 | }
92 |
93 | ${mq.desktop} {
94 | width: 447px;
95 | }
96 | `;
97 |
98 | export const BGImg = styled.div`
99 | display: none;
100 | ${mq.mobile} {
101 | position: absolute;
102 | top: 0;
103 | right: 0;
104 | z-index: -1;
105 | display: block;
106 |
107 | width: 135px;
108 | height: 100vh;
109 | background: linear-gradient(89deg, #040404 1.1%, rgba(4, 4, 4, 0) 70.79%);
110 | background-repeat: no-repeat;
111 | background-position: center;
112 | background-size: 446px auto;
113 | background-image: url(${imgDx2});
114 | }
115 |
116 | ${mq.tablet} {
117 | width: 348px;
118 | background-size: cover;
119 | height: 100%;
120 | }
121 |
122 | ${mq.desktop} {
123 | width: 670px;
124 | height: 100vh;
125 | }
126 | `;
127 |
128 | export const WrapLogo = styled.div`
129 | margin-top: 24px;
130 | margin-bottom: 125px;
131 | `;
132 |
133 | export const Logo = styled.svg`
134 | width: 44px;
135 | height: 17px;
136 | margin-right: 8px;
137 | `;
138 |
139 | export const LogoText = styled.svg`
140 | width: 100px;
141 | height: 17px;
142 | `;
143 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import 'overlayscrollbars/overlayscrollbars.css';
2 | import { Route, Routes, useLocation } from 'react-router-dom';
3 | import { lazy, useEffect } from 'react';
4 | import { useDispatch } from 'react-redux';
5 | import { fetchCurrentUser } from './redux/auth/operation';
6 |
7 | import SharedLayout from './components/SharedLayout/SharedLayout';
8 |
9 | import { PrivateRoute, PublicRoute } from './components/Routes';
10 | import { UseAuth } from './hooks/useAuth';
11 |
12 | const Home = lazy(() => import('../src/pages/Home/Home'));
13 | const SignIn = lazy(() => import('../src/pages/SignIn/SignIn'));
14 | const SignUp = lazy(() => import('../src/pages/SignUp/SignUp'));
15 | const Products = lazy(() => import('../src/pages/Products/Products'));
16 |
17 | const Params = lazy(() => import('../src/pages/Params'));
18 |
19 | const Exercises = lazy(() => import('../src/pages/Exercises/Exercises'));
20 |
21 | const Error = lazy(() => import('../src/pages/Error/Error'));
22 | const Diary = lazy(() => import('../src/pages/Diary/Diary'));
23 | const Profile = lazy(() => import('./pages/Profile/Profile'));
24 |
25 | // const test = import.meta.env.VITE_API_TEST;
26 |
27 | function App() {
28 | const dispatch = useDispatch();
29 |
30 | const { isRefreshing, isLoggedIn } = UseAuth();
31 | const { pathname } = useLocation();
32 |
33 | if (isLoggedIn && pathname !== '/') {
34 | localStorage.setItem('location', pathname);
35 | }
36 |
37 | const location = localStorage.getItem('location');
38 |
39 | useEffect(() => {
40 | dispatch(fetchCurrentUser());
41 | }, [dispatch]);
42 |
43 | // console.log(test);
44 |
45 | return (
46 | !isRefreshing && (
47 |
48 | }>
49 | } redirectTo={location} />}
52 | />
53 | } redirectTo={'/diary'} />
57 | }
58 | />
59 | } redirectTo={'/params'} />
63 | }
64 | />
65 | } redirectTo="/" />}
68 | />
69 | } redirectTo="/" />}
72 | />
73 | } redirectTo="/" />}
76 | />
77 | } redirectTo="/" />}
80 | />
81 | } redirectTo="/" />}
84 | />
85 | } />
86 |
87 |
88 | )
89 | );
90 | }
91 | export default App;
92 |
--------------------------------------------------------------------------------
/src/components/ParamsBlockСard/ParamsBlockCard.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import {
4 | GreyCard,
5 | GreySvgWrapper,
6 | GreySvg,
7 | GreyTextWrapper,
8 | GreyDynamicText,
9 | GreyStaticText,
10 | OrangeCard,
11 | OrangeSvgWrapper,
12 | OrangeSvg,
13 | OrangeTextWrapper,
14 | OrangeDynamicText,
15 | OrangeStaticText,
16 | } from './ParamsBlockCard.styled';
17 |
18 | const ParamsBlockCard = ({ data, measure, type, step, page }) => {
19 | if (type === 'grey') {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {data}
29 | Video tutorial
30 |
31 |
32 | );
33 | }
34 |
35 | if (type === 'orange') {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {data}
46 | {measure}
47 |
48 |
49 | );
50 | }
51 | };
52 |
53 | ParamsBlockCard.propTypes = {
54 | data: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
55 | type: PropTypes.string,
56 | measure: PropTypes.string,
57 | step: PropTypes.string,
58 | page: PropTypes.string,
59 | };
60 |
61 | export default ParamsBlockCard;
62 |
--------------------------------------------------------------------------------
/src/components/UserCard/UserCard.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { handleLogout } from '../../utils';
3 | import DailyStatsCards from '../DailyStatsCards/DailyStatsCards';
4 | import {
5 | AddUserBtn,
6 | Avatar,
7 | ImgWrap,
8 | AvatarWrapper,
9 | CardsWrap,
10 | DailyStatsWrap,
11 | H3,
12 | Container,
13 | UserSVG,
14 | Button,
15 | } from "./UserCard.styled";
16 | import sprite from "../../assets/sprite.svg";
17 | import DescriptionText from "../DescriptionText/DescriptionText";
18 | import PropTypes from "prop-types";
19 |
20 | import { mgForDiary } from "../../utils/descriptionTextMargin";
21 | import { useState } from "react";
22 | import { useSelector } from "react-redux";
23 | import { selectUser } from "../../redux/auth/selectors";
24 |
25 | export default function UserCard({ setAvatar }) {
26 | const { name, avatarURL, dailyСalories, dailyTime } = useSelector(selectUser);
27 | const [imgURL, setImgUrl] = useState(avatarURL || null);
28 | const dispatch = useDispatch();
29 |
30 | const handleChange = (e) => {
31 | setImgUrl(URL.createObjectURL(e.target.files[0]));
32 | setAvatar(e.target.files[0]);
33 | URL.revokeObjectURL(imgURL);
34 | };
35 |
36 | return (
37 |
38 |
39 | {imgURL ? (
40 |
41 |
42 |
43 | ) : (
44 |
45 |
46 |
47 | )}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {name ? name : "user"}
58 |
59 |
60 |
61 |
67 |
68 |
69 |
70 |
76 |
77 |
78 |
79 |
84 |
85 |
96 |
97 | );
98 | }
99 |
100 | UserCard.propTypes = {
101 | setAvatar: PropTypes.func.isRequired,
102 | };
103 |
--------------------------------------------------------------------------------
/src/redux/diary/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { deleteExercise, deleteProduct, getDiaryList } from './operations';
3 |
4 | const contactsInitialValue = {
5 | isLoading: false,
6 | isLoadingExercies: false,
7 | isLoadingProducts: false,
8 | error: null,
9 | productsAndExercisesError: null,
10 | burnedCalories: 0,
11 | consumedCalories: 0,
12 | doneExercisesTime: 0,
13 | products: [],
14 | exercises: [],
15 | };
16 |
17 | const handlePending = state => {
18 | state.isLoading = true;
19 | state.error = null;
20 | };
21 |
22 | const handleFullfield = state => {
23 | state.isLoading = false;
24 | state.error = null;
25 | };
26 |
27 | const handleRejected = (state, payload) => {
28 | state.isLoading = false;
29 | state.error = payload.error;
30 | };
31 |
32 | const diary = createSlice({
33 | name: 'diary',
34 | initialState: contactsInitialValue,
35 | extraReducers: builder => {
36 | builder.addCase(getDiaryList.pending, handlePending);
37 | builder.addCase(getDiaryList.fulfilled, (state, { payload }) => {
38 | state.isLoading = false;
39 | state.products = payload.products || [];
40 | state.exercises = payload.exercises || [];
41 | state.burnedCalories = payload.burnedCalories || 0;
42 | state.consumedCalories = payload.consumedCalories || 0;
43 | state.doneExercisesTime = payload.doneExercisesTime || 0;
44 | });
45 | builder.addCase(getDiaryList.rejected, (state, { payload }) => {
46 | state.productsAndExercisesError = payload;
47 | state.isLoading = false;
48 | state.products = [];
49 | state.exercises = [];
50 | state.burnedCalories = 0;
51 | state.consumedCalories = 0;
52 | state.doneExercisesTime = 0;
53 | });
54 |
55 | builder.addCase(deleteProduct.pending, state => {
56 | state.isLoadingProducts = true;
57 | state.error = null;
58 | });
59 |
60 | builder.addCase(deleteProduct.fulfilled, (state, { payload }) => {
61 | handleFullfield(state);
62 | state.isLoadingProducts = false;
63 | const newProductsList = state.products.filter(
64 | product => product._id !== payload.productId,
65 | );
66 | state.products = newProductsList;
67 | state.isLoadingProducts = false;
68 | state.consumedCalories -= payload.calories;
69 | });
70 | builder.addCase(deleteProduct.rejected, state => {
71 | state.isLoadingExercies = false;
72 | state.error = payload.error;
73 | });
74 |
75 | builder.addCase(deleteExercise.pending, state => {
76 | state.isLoadingExercies = true;
77 | state.error = null;
78 | });
79 | builder.addCase(deleteExercise.fulfilled, (state, { payload }) => {
80 | handleFullfield(state);
81 | state.isLoadingExercies = false;
82 | const newExercisesList = state.exercises.filter(
83 | exercise => exercise._id !== payload.exerciseId,
84 | );
85 | state.exercises = newExercisesList;
86 |
87 | state.burnedCalories -= payload.calories;
88 |
89 | state.doneExercisesTime -= payload.time;
90 | });
91 | builder.addCase(deleteExercise.rejected, handleRejected);
92 | },
93 | });
94 |
95 | export const diaryReducer = diary.reducer;
96 |
--------------------------------------------------------------------------------
/src/components/UserForm/UserForm.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors, button } from '../../utils';
3 |
4 | export const Form = styled.form`
5 | flex-shrink: 0;
6 | max-width: 704px;
7 | margin: 18px auto 0;
8 | `;
9 |
10 | export const InputGroup = styled.div`
11 | ${mq.tablet} {
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: flex-end;
15 | gap: 0 14px;
16 | }
17 | `;
18 |
19 | export const InputSecondGroup = styled.div`
20 | display: flex;
21 | justify-content: space-between;
22 | align-items: flex-end;
23 | gap: 0 14px;
24 | `;
25 |
26 | export const InputThirdGroup = styled(InputSecondGroup)`
27 | ${mq.tablet} {
28 | justify-content: flex-start;
29 | gap: 0 32px;
30 | }
31 | `;
32 |
33 | export const Label = styled.label`
34 | display: block;
35 | max-width: 345px;
36 | width: 100%;
37 | margin-bottom: 21px;
38 | font-size: 12px;
39 | line-height: 1.5;
40 | color: ${colors.textWhite05};
41 | position: relative;
42 |
43 | & p {
44 | margin-bottom: 4px;
45 | }
46 | `;
47 |
48 | export const HalfLabel = styled(Label)`
49 | max-width: 168px;
50 | width: 50%;
51 | `;
52 |
53 | export const Input = styled.input`
54 | width: 100%;
55 | height: 100%;
56 | padding: 14px;
57 | color: ${colors.grey};
58 | background-color: inherit;
59 | border: 1px solid ${colors.textWhite03};
60 | border-radius: 12px;
61 | transition: border-color 0.3s;
62 |
63 | &[data-touch] {
64 | border-color: ${colors.textSuccess};
65 | }
66 |
67 | &[aria-invalid] {
68 | border-color: ${colors.textError}!important;
69 | color: ${colors.textError}!important;
70 | }
71 |
72 | &:disabled {
73 | color: ${colors.textWhite06};
74 | }
75 |
76 | &:hover:not([disabled]),
77 | &:focus:not([disabled]){
78 | border-color: ${colors.orange};
79 | outline: none;
80 | }
81 | `;
82 |
83 | export const ErrorField = styled.span`
84 | position: absolute;
85 | top: 100%;
86 | left: 5px;
87 | max-width: calc(100% - 10px);
88 | font-size: 12px;
89 | line-height: 1;
90 | color: ${colors.textError};
91 | `;
92 |
93 | export const Caption = styled.p`
94 | margin-bottom: 4px;
95 | font-size: 12px;
96 | line-height: 1.5;
97 | color: ${colors.textWhite05};
98 | `;
99 |
100 | export const CheckboxListLine = styled.div`
101 | position: relative;
102 | `;
103 |
104 | export const CheckboxList = styled.div`
105 | position: relative;
106 | margin: 42px 0 40px;
107 |
108 | ${mq.tablet} {
109 | margin: 32px 0 38px;
110 | }
111 |
112 | ${mq.desktop} {
113 | margin-bottom: 48px;
114 | }
115 | `;
116 |
117 | export const Button = styled.button`
118 | ${button};
119 |
120 | min-width: 115px;
121 | padding: 12px;
122 | color: ${colors.white};
123 | background-color: ${colors.orange};
124 | border-radius: 12px;
125 | transition:
126 | color 0.3s,
127 | background-color 0.3s;
128 |
129 | &:hover:not([disabled]),
130 | &:focus:not([disabled]) {
131 | background-color: ${colors.orangeSecondary};
132 | }
133 |
134 | &:disabled {
135 | color: ${colors.textWhite06};
136 | cursor: default;
137 | }
138 | `;
139 |
--------------------------------------------------------------------------------
/src/components/MobMenu/MobMenu.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { mq, colors, button, svgUser } from '../../utils';
3 | import { NavLink } from 'react-router-dom';
4 |
5 | export const Link = styled(NavLink)`
6 | display: flex;
7 | align-items: center;
8 | ${mq.desktop} {
9 | display: none;
10 | }
11 | `;
12 |
13 | export const MenuBars = styled.div`
14 | display: flex;
15 | margin-right: 20px;
16 | gap: 14px;
17 |
18 | ${mq.tablet} {
19 | margin-right: 32px;
20 | gap: 16px;
21 | }
22 | `;
23 |
24 | export const ButtonMenu = styled.button`
25 | display: flex;
26 | align-items: center;
27 | gap: 8px;
28 | ${button}
29 | ${mq.desktop} {
30 | display: none;
31 | }
32 | `;
33 |
34 | export const AvatarWrapper = styled.div`
35 | width: 37px;
36 | height: 37px;
37 | border-radius: 50%;
38 | overflow: hidden;
39 | border: 1px solid ${colors.orange};
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 |
44 | > img {
45 | width: 100%;
46 | height: auto;
47 | }
48 | ${mq.tablet} {
49 | width: 46px;
50 | height: 46px;
51 | }
52 | ${mq.desktop} {
53 | display: none;
54 | }
55 | `;
56 |
57 | export const Svg = styled.svg`
58 | stroke: ${colors.grey};
59 | width: 24px;
60 | height: 24px;
61 |
62 | ${mq.tablet} {
63 | height: 32px;
64 | width: 32px;
65 | }
66 | `;
67 | export const SvgUser = styled.svg`
68 | @media (max-width: 376px) {
69 | width: 37px;
70 | height: 37px;
71 | }
72 | ${svgUser}
73 | `;
74 |
75 | export const ButtonMenuExit = styled.button`
76 | ${button}
77 | margin-left: auto;
78 | `;
79 |
80 | export const Span = styled.span`
81 | display: flex;
82 | align-items: center;
83 | flex-direction: row-reverse;
84 | color: ${colors.white};
85 | font-family: Roboto;
86 | font-size: 14px;
87 |
88 | ${mq.tablet} {
89 | color: ${colors.white};
90 | font-size: 18px;
91 | line-height: 24px;
92 | }
93 | `;
94 |
95 | export const NavMenuItems = styled.div`
96 | display: flex;
97 | width: 100%;
98 | background-color: ${colors.orange};
99 | padding: 20px 20px 0px 20px;
100 | flex-direction: column;
101 | justify-content: space-between;
102 | `;
103 | export const NavbarToggle = styled.div`
104 | width: 100%;
105 | height: 80px;
106 | display: flex;
107 | justify-content: start;
108 | align-items: center;
109 | `;
110 |
111 | export const ContainerMenu = styled.div`
112 | position: relative;
113 |
114 | width: 100%;
115 | height: 100vh;
116 | display: flex;
117 | justify-content: center;
118 | position: fixed;
119 | top: 0;
120 | right: -100%;
121 | transition: 850ms;
122 | z-index: 999;
123 |
124 | ${mq.tablet} {
125 | width: 350px;
126 | }
127 |
128 | &.active {
129 | right: 0;
130 | transition: 350ms;
131 | }
132 | `;
133 |
134 | export const ContainerLink = styled.ul`
135 | display: flex;
136 | align-items: center;
137 | flex-direction: column;
138 | gap: 20px;
139 | `;
140 |
141 | export const Overlay = styled.div`
142 | position: absolute;
143 | top: 0;
144 | left: 0;
145 | width: 100%;
146 | height: 100vh;
147 | background-color: rgba(0, 0, 0, 0.5);
148 | z-index: 99;
149 | `;
150 |
--------------------------------------------------------------------------------
/src/redux/auth/operation.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { toast } from 'react-toastify';
4 |
5 | axios.defaults.baseURL = 'https://power-pulse-rest-api.onrender.com';
6 |
7 | const token = {
8 | set: token => {
9 | axios.defaults.headers.common.Authorization = `Bearer ${token}`;
10 | },
11 | unSet: () => {
12 | axios.defaults.headers.common.Authorization = '';
13 | },
14 | };
15 |
16 | export const authUser = createAsyncThunk(
17 | 'addUserStatus',
18 | async (user, { rejectWithValue }) => {
19 | try {
20 | const { data } = await axios.post('/api/users/register', user);
21 |
22 | token.set(data.token);
23 | return data;
24 | } catch (error) {
25 | toast.error('Oops... Something went wrong! Try again!');
26 | return rejectWithValue(error.response.data.message);
27 | }
28 | },
29 | );
30 |
31 | export const logInUser = createAsyncThunk(
32 | 'logInStatus',
33 | async (user, { rejectWithValue }) => {
34 | try {
35 | const { data } = await axios.post('/api/users/login', user);
36 |
37 | token.set(data.token);
38 | return data;
39 | } catch (error) {
40 | toast.error(
41 | 'Oops... Something went wrong! Enter correct"email" and "password" or sign up, please',
42 | );
43 | return rejectWithValue(
44 | 'Oops... Something went wrong! Enter correct"email" and "password", please',
45 | );
46 | }
47 | },
48 | );
49 |
50 | export const logOutUser = createAsyncThunk(
51 | 'logOutStatus',
52 | async (_, { rejectWithValue }) => {
53 | try {
54 | await axios.post('/api/users/logout');
55 | token.unSet();
56 | } catch (error) {
57 | toast.error('Oops, something went wrong((( Try again, please!');
58 | return rejectWithValue(
59 | 'Oops, something went wrong((( Try again, please!',
60 | );
61 | }
62 | },
63 | );
64 |
65 | export const fetchCurrentUser = createAsyncThunk(
66 | 'refreshUser',
67 | async (_, { rejectWithValue, getState }) => {
68 | const state = getState();
69 | const persistedToken = state.auth.token;
70 | if (!persistedToken) {
71 | return rejectWithValue();
72 | }
73 | token.set(persistedToken);
74 | try {
75 | const { data } = await axios.get('/api/users/current');
76 |
77 | return data;
78 | } catch (error) {
79 | return rejectWithValue(error);
80 | }
81 | },
82 | );
83 |
84 | export const updateUserData = createAsyncThunk(
85 | 'auth/updateUser',
86 | async (userData, { rejectWithValue }) => {
87 | try {
88 | const result = await axios.patch('/api/users/update', userData, {
89 | headers: {
90 | 'Content-Type': 'multipart/form-data',
91 | },
92 | });
93 | return result.data;
94 | } catch (e) {
95 | return rejectWithValue(e.message);
96 | }
97 | },
98 | );
99 |
100 | export const updateBodyParts = createAsyncThunk(
101 | 'auth/updateBodyParts',
102 | async (userData, { rejectWithValue }) => {
103 | try {
104 | const { data } = await axios.post('/api/users/create', userData);
105 | return data;
106 | } catch (e) {
107 | return rejectWithValue(e.message);
108 | }
109 | },
110 | );
111 |
--------------------------------------------------------------------------------
/src/components/ProductsFilter/ProductsFilter.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, mq } from '../../utils';
3 | import shewron from '../../assets/chevron-down.png';
4 |
5 | export const Option = styled.option`
6 | width: 200px;
7 | padding: 10px;
8 | font-size: 16px;
9 | border: 1px solid #ccc;
10 | border-radius: 4px;
11 | background-color: #fff;
12 | appearance: none;
13 | cursor: pointer;
14 | `;
15 |
16 | export const Select = styled.select`
17 | appearance: none;
18 | position: reletive;
19 |
20 | height: 46px;
21 | width: 100%;
22 |
23 | ${mq.tablet} {
24 | width: 204px;
25 | height: 52px;
26 | }
27 |
28 | padding-left: 14px;
29 | padding-right: 14px;
30 |
31 | border-radius: 12px;
32 | border: 1px solid ${colors.textWhite03};
33 |
34 | font-size: 16px;
35 | font-weight: 400;
36 | line-height: 150%;
37 |
38 | outline: none;
39 |
40 | color: ${colors.textWhite06};
41 | background-color: transparent;
42 | `;
43 |
44 | export const FilterContainer = styled.div`
45 | position: relative;
46 | display: flex;
47 | flex-direction: column;
48 | gap: 16px;
49 | width: 100%;
50 | ${mq.tablet} {
51 | flex-direction: row;
52 | width: 664px;
53 | }
54 | `;
55 |
56 | export const FilterTitle = styled.div`
57 | display: none;
58 | ${mq.desktop} {
59 | display: block;
60 | position: absolute;
61 | top: -24px;
62 | right: 12px;
63 | font-size: 14px;
64 | line-height: 128%;
65 | color: ${colors.textWhite05};
66 | }
67 | `;
68 |
69 | export const SelectContainer = styled.div`
70 | display: flex;
71 | flex-direction: column;
72 | gap: 16px;
73 | ${mq.tablet} {
74 | flex-direction: row;
75 | }
76 | `;
77 |
78 | export const SelectPointer = styled.div`
79 | position: relative;
80 | &::after {
81 | content: ' ';
82 | width: 18px;
83 | height: 18px;
84 | background-image: url(${shewron});
85 | color: red;
86 | position: absolute;
87 | top: 50%;
88 | right: 14px;
89 | transform: translateY(-50%);
90 | pointer-events: none;
91 | }
92 | `;
93 |
94 | export const InputWrapper = styled.form`
95 | position: relative;
96 | width: 100%;
97 | ${mq.tablet} {
98 | width: 236px;
99 | }
100 | `;
101 |
102 | export const TextInput = styled.input`
103 | height: 46px;
104 | width: 100%;
105 | ${mq.tablet} {
106 | height: 52px;
107 | width: 236px;
108 | }
109 | padding-left: 14px;
110 | padding-right: 14px;
111 |
112 | border-radius: 12px;
113 | border: 1px solid ${colors.textWhite03};
114 |
115 | font-size: 16px;
116 | font-weight: 400;
117 | line-height: 150%;
118 | outline: none;
119 |
120 | color: ${colors.textWhite06};
121 | background-color: transparent;
122 |
123 | &:focus-visible {
124 | border: 1px solid ${colors.orange};
125 | }
126 |
127 | &:focus-visible + span > svg {
128 | stroke: ${colors.orange};
129 | }
130 | `;
131 |
132 | export const Svg = styled.svg`
133 | stroke: ${colors.textWhite06};
134 |
135 | width: 18px;
136 | height: 18px;
137 |
138 | &:hover {
139 | stroke: ${colors.orange};
140 | }
141 | `;
142 |
143 | export const SpanForSvg = styled.span`
144 | position: absolute;
145 | top: 15px;
146 | right: 18px;
147 | width: 18px;
148 | height: 18px;
149 | ${mq.tablet} {
150 | top: 17px;
151 | }
152 |
153 | ${mq.desktop} {
154 | top: 18px;
155 | }
156 | `;
157 |
--------------------------------------------------------------------------------
/src/components/UserCard/UserCard.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { colors, button, mq } from '../../utils';
3 |
4 | export const Container = styled.div`
5 | flex-shrink: 0;
6 | max-width: 439px;
7 | margin: 0 auto;
8 | ${mq.desktop} {
9 | margin: 0;
10 | }
11 | `;
12 |
13 | export const AvatarWrapper = styled.div`
14 | position: relative;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | width: 90px;
19 | height: 90px;
20 | margin: 0 auto 32px;
21 | border: 1px solid ${colors.orange};
22 | border-radius: 50%;
23 |
24 | ${mq.tablet} {
25 | width: 150px;
26 | height: 150px;
27 | margin-bottom: 34px;
28 | }
29 | `;
30 |
31 | export const UserSVG = styled.svg`
32 | width: 62px;
33 | height: 62px;
34 | fill: ${colors.grey};
35 | fill-opacity: 0.1;
36 |
37 | ${mq.tablet} {
38 | width: 102px;
39 | height: 102px;
40 | }
41 | `;
42 |
43 | export const ImgWrap = styled.div`
44 | width: 90px;
45 | height: 90px;
46 | border-radius: 50%;
47 | overflow: hidden;
48 |
49 | ${mq.tablet} {
50 | width: 150px;
51 | height: 150px;
52 | }
53 | `;
54 |
55 | export const Avatar = styled.img`
56 | width: 100%;
57 | height: 100%;
58 | object-fit: cover;
59 | object-position: center top;
60 | `;
61 |
62 | export const AddUserBtn = styled.label`
63 | position: absolute;
64 | bottom: -12px;
65 | left: 50%;
66 | display: block;
67 | width: 24px;
68 | height: 24px;
69 | transform: translateX(-50%);
70 |
71 | & input {
72 | position: absolute;
73 | width: 0;
74 | height: 0;
75 | opacity: 0;
76 | visibility: hidden;
77 | }
78 |
79 | & svg {
80 | width: 24px;
81 | height: 24px;
82 | fill: ${colors.orange};
83 | stroke: ${colors.white};
84 | transition: fill 0.3s;
85 | }
86 |
87 | &:hover svg,
88 | &:focus svg {
89 | fill: ${colors.orangeSecondary};
90 | }
91 |
92 | ${mq.tablet} {
93 | bottom: -16px;
94 | width: 32px;
95 | height: 32px;
96 |
97 | & svg {
98 | width: 32px;
99 | height: 32px;
100 | }
101 | }
102 | `;
103 |
104 | export const H3 = styled.h3`
105 | margin-bottom: 40px;
106 | font-size: 18px;
107 | font-weight: 400;
108 | line-height: 1.1;
109 | color: ${colors.grey};
110 | text-align: center;
111 |
112 | ${mq.tablet} {
113 | margin-bottom: 32px;
114 | }
115 | `;
116 |
117 | export const CardsWrap = styled.div`
118 | display: flex;
119 | justify-content: center;
120 | gap: 0 14px;
121 | margin-bottom: 40px;
122 |
123 | ${mq.tablet} {
124 | margin-bottom: 32px;
125 | }
126 | `;
127 |
128 | export const DailyStatsWrap = styled.div`
129 | width: 100%;
130 | `;
131 |
132 | export const Button = styled.button`
133 | ${button};
134 | display: flex;
135 | gap: 0 8px;
136 | margin-top: 40px;
137 | margin-left: auto;
138 | transition: color 0.3s;
139 |
140 | & span {
141 | font-size: 14px;
142 | line-height: 1.3;
143 | color: ${colors.grey};
144 | transition: color 0.3s;
145 | }
146 |
147 | & svg {
148 | width: 20px;
149 | height: 20px;
150 | stroke: ${colors.orange};
151 | transition: stroke 0.3s;
152 | }
153 |
154 | &:hover span,
155 | &:hover svg,
156 | &:focus span,
157 | &:focus svg {
158 | color: ${colors.textWhite08};
159 | stroke: ${colors.orangeSecondary};
160 | }
161 |
162 | ${mq.tablet} {
163 | margin-top: 32px;
164 | }
165 | `;
166 |
--------------------------------------------------------------------------------
/src/components/MobMenu/MobMenu.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { handleLogout } from '../../utils';
4 | import CustomNavLink from '../CustomNavLink/CustomNavLink';
5 | import {
6 | MenuBars,
7 | ButtonMenu,
8 | ButtonMenuExit,
9 | NavMenuItems,
10 | NavbarToggle,
11 | ContainerMenu,
12 | ContainerLink,
13 | Span,
14 | Svg,
15 | Link,
16 | Overlay,
17 | AvatarWrapper,
18 | } from './MobMenu.styled';
19 | import { UserAvatar } from '../headersComp/UserNav/UserNav.styled';
20 | import { selectUser } from '../../redux/auth/selectors';
21 |
22 | import sprite from '../../assets/sprite.svg';
23 |
24 | const MobMenu = () => {
25 | const [mobMenu, setMobMenu] = useState(false);
26 |
27 | const toggleMobMenu = () => setMobMenu(!mobMenu);
28 |
29 | const dispatch = useDispatch();
30 |
31 | const { avatarURL } = useSelector(selectUser);
32 |
33 | useEffect(() => {
34 | const handleKeyDown = e => {
35 | if (e.code === 'Escape' && mobMenu) {
36 | toggleMobMenu();
37 | }
38 | };
39 |
40 | window.addEventListener('keydown', handleKeyDown);
41 |
42 | return () => {
43 | window.removeEventListener('keydown', handleKeyDown);
44 | };
45 | }, [mobMenu, toggleMobMenu]);
46 |
47 | return (
48 | <>
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {avatarURL ? (
57 |
58 |
59 |
60 | ) : (
61 |
62 |
63 |
64 | )}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {mobMenu && }
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {
98 | toggleMobMenu();
99 | handleLogout(dispatch);
100 | }}
101 | >
102 | Logout
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | >
112 | );
113 | };
114 |
115 | export default MobMenu;
116 |
--------------------------------------------------------------------------------
/src/components/ParamsForm/ParamsForm.styled.jsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { Field } from 'formik';
3 | import { colors, mq } from '../../utils';
4 |
5 | export const InputGroup = styled.div`
6 | display: flex;
7 | justify-content: space-around;
8 | flex-wrap: wrap;
9 |
10 | margin: -7px;
11 | margin-top: 50px;
12 | margin-bottom: 30px;
13 |
14 | ${mq.tablet} {
15 | justify-content: flex-start;
16 | width: 527px;
17 | margin-top: 54px;
18 | margin-bottom: 32px;
19 | }
20 | `;
21 |
22 | export const FormikField = styled(Field)`
23 | width: 275px;
24 |
25 | ${mq.mobile} {
26 | width: 155px;
27 | }
28 | height: 52px;
29 | padding: 14px 0 14px 14px;
30 | margin: 7px;
31 |
32 | font-size: 14px;
33 | line-height: 128.571%;
34 |
35 | border: 1px solid rgba(239, 237, 232, 0.3);
36 | border-radius: 12px;
37 | outline: none;
38 |
39 | background-color: transparent;
40 | color: ${colors.textWhite06};
41 |
42 | transition: border-color 250ms cubic-bezier(0.4, 0, 0.2, 1);
43 |
44 | &[data-touch=true]{
45 | border-color: ${colors.textSuccess};
46 | }
47 |
48 | &::placeholder {
49 | color: ${colors.textWhite06};
50 | }
51 |
52 | &:focus-within {
53 | border-color: ${colors.orange};
54 | }
55 |
56 | &::-webkit-outer-spin-button,
57 | ::-webkit-inner-spin-button {
58 | -webkit-appearance: none;
59 | margin: 0;
60 | }
61 |
62 | /* &[type='number'] {
63 | -moz-appearance: textfield;
64 | } */
65 |
66 | ${mq.tablet} {
67 | font-size: 16px;
68 | line-height: 150%;
69 | }
70 | `;
71 |
72 | export const FormRadioBtnGroupWrapper = styled.div`
73 | display: flex;
74 | margin-bottom: 28px;
75 | padding-left: 2px;
76 |
77 | ${mq.tablet} {
78 | margin-bottom: 32px;
79 | }
80 | `;
81 |
82 | export const BtnsAndBlock = styled.div`
83 | display: flex;
84 | align-items: flex-start;
85 | margin-bottom: 159px;
86 |
87 | ${mq.tablet} {
88 | margin-bottom: 283px;
89 | }
90 | ${mq.desktop} {
91 | margin-bottom: 48px;
92 | }
93 | `;
94 |
95 | export const BtnSubmit = styled.button`
96 | margin-top: 28px;
97 | margin-right: 16px;
98 | padding: 12px 40px;
99 |
100 | font-size: 16px;
101 | font-weight: 500;
102 | line-height: 112.5%;
103 |
104 | color: ${colors.white};
105 | background: ${colors.orange};
106 |
107 | border: none;
108 | border-radius: 12px;
109 |
110 | transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
111 |
112 | &:hover {
113 | transform: scale(0.99);
114 | }
115 |
116 | &:focus {
117 | transform: scale(0.99);
118 | }
119 |
120 | ${mq.tablet} {
121 | margin-top: 64px;
122 | padding: 16px 75px;
123 |
124 | font-size: 20px;
125 | line-height: 120%;
126 | }
127 | `;
128 |
129 | export const CalendarWrapper = styled.div`
130 | position: relative;
131 | width: 155px;
132 | height: 52px;
133 | margin: 7px;
134 | `;
135 |
136 | export const CalendarPlaceholder = styled.p`
137 | position: absolute;
138 | pointer-events: none;
139 | color: ${colors.textWhite06};
140 | top: 25%;
141 | left: 10%;
142 |
143 | font-size: 14px;
144 | line-height: 128.571%;
145 |
146 | ${mq.tablet} {
147 | font-size: 16px;
148 | line-height: 150%;
149 | }
150 | `;
151 |
152 | export const RADIO_STYLE_OPTIONS = {
153 | color: 'grey',
154 | '&.Mui-checked': {
155 | color: colors.orangeSecondary,
156 | },
157 | '& .MuiSvgIcon-root': {
158 | fontSize: 18,
159 | },
160 | };
161 |
162 | export const RADIO_TITLE_STYLE = {
163 | color: `${colors.white}`,
164 | fontSize: '14px',
165 | fontWeight: 400,
166 | lineHeight: '128.571%',
167 | };
168 |
169 | export const ErrorMessage = styled.p`
170 | width: 155px;
171 | font-size: 11px;
172 | padding-left: ${props => (props.padding ? 10 : 0)}px;
173 | color: ${colors.red};
174 | `;
175 |
--------------------------------------------------------------------------------