├── .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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 11 | 12 | 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 | 14 | 15 | {' '} 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 | 22 | 23 | 24 | 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 | <BlockWrapper> 30 | <UserCard setAvatar={setAvatarFile} /> 31 | 32 | <FormWrap> 33 | <UserForm submit={handleSubmit} avatar={avatarFile} /> 34 | </FormWrap> 35 | </BlockWrapper> 36 | </Container> 37 | </main> 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 | <Backdrop onClick={handleBackdropClick}> 31 | <ModalWrap width={width} height={height}> 32 | <ButtonExit type="button" onClick={() => openModal()}> 33 | <Svg> 34 | <use href={sprite + '#close'} /> 35 | </Svg> 36 | </ButtonExit> 37 | 38 | {children} 39 | </ModalWrap> 40 | </Backdrop>, 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 | <DayDiaryContainer> 17 | <DayDiarySubDiv> 18 | <DayDiarySubTitle> 19 | {isDayProducts ? 'Products' : 'Exercises'} 20 | </DayDiarySubTitle> 21 | <AddLink to={to}> 22 | Add {isDayProducts ? 'product' : 'exercise'} 23 | <ArrowRight> 24 | <use href={sprite + `#arrow-right`}></use> 25 | </ArrowRight> 26 | </AddLink> 27 | </DayDiarySubDiv> 28 | {products.length !== 0 ? ( 29 | <ProductsTable products={products} /> 30 | ) : ( 31 | <DayNoContentText> 32 | Not found {isDayProducts ? 'products' : 'exercises'} 33 | </DayNoContentText> 34 | )} 35 | </DayDiaryContainer> 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 | <Item onClick={() => handleGetExercises(requestString, name)}> 32 | <WrapCard> 33 | <Image src={imgURL} alt={name} /> 34 | <WrapSpan> 35 | <Name>{name}</Name> 36 | <Filter>{filter}</Filter> 37 | </WrapSpan> 38 | </WrapCard> 39 | </Item> 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 | <main> 20 | <Container> 21 | <Content> 22 | <WrapLogo> 23 | <Logo> 24 | <use href={sprite + `#icon-logo_error`}></use> 25 | </Logo> 26 | <LogoText> 27 | <use href={sprite + `#icon-PowerPulse`}></use> 28 | </LogoText> 29 | </WrapLogo> 30 | <Title>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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | user's avatar 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 |
    26 | 27 | 30 | 31 | setQuantity(e.target.value)} 36 | /> 37 | 38 | 39 | 40 | 41 | 42 | Calories: {amount} 43 | 44 | 45 | 46 | 0 ? false : true} 48 | type="button" 49 | onClick={() => 50 | addProduct({ 51 | id: data.id, 52 | date, 53 | amount: quantity, 54 | calories: amount, 55 | }) 56 | } 57 | > 58 | Add to diary 59 | 60 | 61 | Cancel 62 | 63 | 64 |
    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 | <SubTitle 38 | text={ 39 | 'Welcome! Please enter your credentials to login to the platform:' 40 | } 41 | /> 42 | <AuthForm btnTitle="Sign In" nameIsShown={false} onSubmit={logIn} /> 43 | <BtnSubtitle 44 | text={'Don’t have an account?'} 45 | to={'/signup'} 46 | linkText={'Sign Up'} 47 | /> 48 | 49 | <ParamsBlockCard 50 | type={'grey'} 51 | page={'auth'} 52 | data={videoExercisesCount} 53 | /> 54 | 55 | <ParamsBlockCard 56 | type={'orange'} 57 | page={'auth'} 58 | data={allBurnedCalories} 59 | measure={'cal'} 60 | /> 61 | </Wrapper> 62 | </main> 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 | <Label onClick={onClick} ref={ref}> 25 | <Ipt 26 | value={value || ''} 27 | name="name" 28 | readOnly 29 | withoutВorder={withoutВorder} 30 | /> 31 | <Icon> 32 | <use href={`${sprite}#calendar`}></use> 33 | </Icon> 34 | </Label> 35 | ); 36 | }); 37 | 38 | ExampleCustomInput.displayName = 'Label'; 39 | 40 | return ( 41 | <> 42 | <DatePicker 43 | name={name} 44 | selected={typeof value == 'string' ? parseISO(value) : value} 45 | onChange={date => { 46 | onChange(name, date); 47 | }} 48 | maxDate={maxDate} 49 | minDate={minDate} 50 | yearDropdownItemNumber={40} 51 | customInput={customInput || <ExampleCustomInput />} 52 | scrollableYearDropdown 53 | dateFormat={dateFormat || 'dd.MM.yyyy'} 54 | showYearDropdown={showYearDropdown} 55 | /> 56 | <Global styles={GlobalStyles} /> 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 | <li key={nanoid()}> 37 | <Key> 38 | {`${transformKey(key)} `} 39 | <Value>{key === "time" && `${data[key]} seconds` } {key === "burnedCalories" && `${data[key]}`} {key === "calories" && `${data[key]}`} </Value> 40 | </Key> 41 | </li> 42 | ); 43 | }); 44 | return renderKeys; 45 | }; 46 | 47 | return ( 48 | <ContentWrap> 49 | <Img 50 | src={modalType === 'product' ? foodIcon : thumbUp} 51 | alt="it`s a placeholder image, but it`s avocado too" 52 | /> 53 | <WellDone>Well Done</WellDone> 54 | 55 | <DataList>{mappedData(data)}</DataList> 56 | 57 | <BtnSubmit 58 | title={modalType === 'product' ? 'Next product' : 'Next exercise'} 59 | btnNext={btnNext} 60 | /> 61 | 62 | <ToDiary to={'/diary'}> 63 | To the diary 64 | <Svg fill=""> 65 | <use href={sprite + `#arrow-right`}></use> 66 | </Svg> 67 | </ToDiary> 68 | </ContentWrap> 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 | <main> 34 | <WrapperDesktop></WrapperDesktop> 35 | <Wrapper> 36 | <Title text={'Sign Up'} margin={mg} /> 37 | <SubTitle 38 | text={ 39 | 'Thank you for your interest in our platform. To complete the registration process, please provide us with the following information.' 40 | } 41 | /> 42 | <AuthForm 43 | nameIsShown={true} 44 | btnTitle="Sign Up" 45 | onSubmit={handleSubmit} 46 | /> 47 | 48 | <BtnSubtitle 49 | text={'Already have account?'} 50 | to={'/signin'} 51 | linkText={'Sign In'} 52 | /> 53 | 54 | <ParamsBlockCard 55 | type={'grey'} 56 | page={'auth'} 57 | data={videoExercisesCount} 58 | /> 59 | 60 | <ParamsBlockCard 61 | type={'orange'} 62 | page={'auth'} 63 | data={allBurnedCalories} 64 | measure={'cal'} 65 | /> 66 | </Wrapper> 67 | </main> 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 | <CategoriesList> 38 | <CategoriesListItem> 39 | <CategoriesLink 40 | type="button" 41 | onClick={handleCategoryChange} 42 | className={activeCategory === 'Body parts' ? 'active' : ''} // Применяем класс "active" к активной кнопке 43 | > 44 | Body parts 45 | </CategoriesLink> 46 | </CategoriesListItem> 47 | <CategoriesListItem> 48 | <CategoriesLink 49 | type="button" 50 | onClick={handleCategoryChange} 51 | className={activeCategory === 'Muscles' ? 'active' : ''} 52 | > 53 | Muscles 54 | </CategoriesLink> 55 | </CategoriesListItem> 56 | <CategoriesListItem> 57 | <CategoriesLink 58 | type="button" 59 | onClick={handleCategoryChange} 60 | className={activeCategory === 'Equipment' ? 'active' : ''} 61 | > 62 | Equipment 63 | </CategoriesLink> 64 | </CategoriesListItem> 65 | </CategoriesList> 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 | <DayDiaryContainer marginBottom={marginBottom}> 38 | <DayDiarySubDiv> 39 | <DayDiarySubTitle> 40 | {productTable ? 'Products' : 'Exercises'} 41 | </DayDiarySubTitle> 42 | <AddLink to={to}> 43 | Add {productTable ? 'product' : 'exercise'} 44 | <ArrowRight> 45 | <use href={sprite + `#arrow-right`}></use> 46 | </ArrowRight> 47 | </AddLink> 48 | </DayDiarySubDiv> 49 | 50 | {list.length !== 0 ? ( 51 | <> 52 | <TableForDiary 53 | list={list} 54 | productTable={productTable} 55 | exerciseTable={exerciseTable} 56 | onDelete={handleDelete} 57 | date={date} 58 | /> 59 | <TableForDiaryOnMobile 60 | list={list} 61 | productTable={productTable} 62 | exerciseTable={exerciseTable} 63 | onDelete={handleDelete} 64 | date={date} 65 | /> 66 | </> 67 | ) : ( 68 | <DayNoContentText> 69 | Not found {productTable ? 'products' : 'exercises'} 70 | </DayNoContentText> 71 | )} 72 | </DayDiaryContainer> 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 | <Swiper 56 | key={category} 57 | modules={[Navigation, Pagination, Scrollbar, A11y]} 58 | spaceBetween={50} 59 | slidesPerView={1} 60 | pagination={{ clickable: true }} 61 | className="pagination" 62 | > 63 | {chunkedFilters.map(arr => ( 64 | <SwiperSlide key={nanoid()}> 65 | <ExercisesItemList> 66 | {arr.map(({ filter, name, imgURL, _id }) => ( 67 | <ExercisesItem 68 | handleGetExercises={handleGetExercises} 69 | key={_id} 70 | imgURL={imgURL} 71 | name={capitalizeWord(name)} 72 | filter={filter} 73 | /> 74 | ))} 75 | </ExercisesItemList> 76 | </SwiperSlide> 77 | ))} 78 | </Swiper> 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 | <main> 53 | <ProductPageContainer> 54 | <FlexWrapper> 55 | <Title text="Products" /> 56 | <ProductsFilter /> 57 | </FlexWrapper> 58 | {products.length !== 0 ? ( 59 | <ScrollBar width={{ dt: '878' }}> 60 | <InfiniteScroll 61 | pageStart={0} 62 | loadMore={onLoadMore} 63 | hasMore={hasMore && !isLoadingMoreProducts} 64 | loader={<Loader key={'qwe789'} size={'60'} />} 65 | useWindow={false} 66 | > 67 | <ProductsOrExercisesContainer> 68 | {products.map(product => { 69 | return ( 70 | <ProductsOrExercisesItem 71 | key={product.id} 72 | page="product" 73 | data={product} 74 | /> 75 | ); 76 | })} 77 | </ProductsOrExercisesContainer> 78 | </InfiniteScroll> 79 | </ScrollBar> 80 | ) : ( 81 | <EmptyProductList /> 82 | )} 83 | </ProductPageContainer> 84 | </main> 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 | <Routes> 48 | <Route path={'/'} element={<SharedLayout />}> 49 | <Route 50 | index 51 | element={<PublicRoute component={<Home />} redirectTo={location} />} 52 | /> 53 | <Route 54 | path="/signin" 55 | element={ 56 | <PublicRoute component={<SignIn />} redirectTo={'/diary'} /> 57 | } 58 | /> 59 | <Route 60 | path="/signup" 61 | element={ 62 | <PublicRoute component={<SignUp />} redirectTo={'/params'} /> 63 | } 64 | /> 65 | <Route 66 | path="/products" 67 | element={<PrivateRoute component={<Products />} redirectTo="/" />} 68 | /> 69 | <Route 70 | path="/params" 71 | element={<PrivateRoute component={<Params />} redirectTo="/" />} 72 | /> 73 | <Route 74 | path="/exercises" 75 | element={<PrivateRoute component={<Exercises />} redirectTo="/" />} 76 | /> 77 | <Route 78 | path="/diary" 79 | element={<PrivateRoute component={<Diary />} redirectTo="/" />} 80 | /> 81 | <Route 82 | path="/profile" 83 | element={<PrivateRoute component={<Profile />} redirectTo="/" />} 84 | /> 85 | <Route path="*" element={<Error />} /> 86 | </Route> 87 | </Routes> 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 | <GreyCard step={step} page={page}> 22 | <GreySvgWrapper> 23 | <GreySvg viewBox="0 0 15 18"> 24 | <path d="M15 9L0 17.6603L0 0.339746L15 9Z" /> 25 | </GreySvg> 26 | </GreySvgWrapper> 27 | <GreyTextWrapper> 28 | <GreyDynamicText>{data}</GreyDynamicText> 29 | <GreyStaticText>Video tutorial</GreyStaticText> 30 | </GreyTextWrapper> 31 | </GreyCard> 32 | ); 33 | } 34 | 35 | if (type === 'orange') { 36 | return ( 37 | <OrangeCard page={page}> 38 | <OrangeSvgWrapper> 39 | <OrangeSvg viewBox="0 0 32 32"> 40 | <path d="M29.647 9.451c-0.419-0.501-1.166-0.567-1.667-0.149l-3.282 2.757-1.509-3.739c-0.054-0.14-0.135-0.258-0.229-0.362-0.308-0.686-0.844-1.275-1.582-1.617-0.32-0.146-0.652-0.224-0.982-0.262-0.073-0.038-0.139-0.088-0.222-0.114l-5.775-1.61c-0.324-0.088-0.65-0.031-0.917 0.125-0.317 0.107-0.589 0.338-0.719 0.672l-2.175 5.592c-0.236 0.608 0.066 1.294 0.676 1.534 0.607 0.236 1.294-0.068 1.532-0.678l1.837-4.722 2.63 0.731c-0.064 0.104-0.133 0.201-0.185 0.312l-3.372 7.309c-0.049 0.107-0.075 0.217-0.109 0.328l-4.098 6.871-6.859 2.294c-0.776 0.58-0.941 1.674-0.367 2.45 0.577 0.778 1.674 0.943 2.448 0.369l7.018-2.417c0.215-0.156 0.371-0.36 0.489-0.58 0.088-0.094 0.189-0.168 0.256-0.284l2.443-4.096 4.337 3.696-4.641 5.23c-0.639 0.721-0.575 1.832 0.149 2.469 0.723 0.643 1.83 0.575 2.473-0.149l5.791-6.524c0.18-0.201 0.288-0.433 0.36-0.676 0.043-0.132 0.043-0.269 0.054-0.406 0-0.069 0.026-0.132 0.021-0.196-0.016-0.478-0.21-0.943-0.601-1.274l-3.991-3.403c0.288-0.274 0.532-0.6 0.709-0.983l2.585-5.599 0.828 2.206c0.035 0.196 0.1 0.388 0.239 0.549 0.125 0.149 0.284 0.248 0.454 0.317 0.017 0.009 0.038 0.010 0.059 0.016 0.107 0.038 0.216 0.075 0.329 0.080 0.133 0.012 0.269-0.005 0.405-0.043 0.004-0.002 0.005-0.002 0.005-0.002 0.036-0.009 0.073-0.002 0.109-0.017 0.192-0.073 0.34-0.196 0.466-0.34l4.71-3.998c0.501-0.421 0.289-1.166-0.132-1.667z"></path> 41 | <path d="M23.689 6.602c1.823 0 3.301-1.478 3.301-3.301s-1.478-3.301-3.301-3.301c-1.823 0-3.301 1.478-3.301 3.301s1.478 3.301 3.301 3.301z"></path> 42 | </OrangeSvg> 43 | </OrangeSvgWrapper> 44 | <OrangeTextWrapper> 45 | <OrangeDynamicText>{data}</OrangeDynamicText> 46 | <OrangeStaticText>{measure}</OrangeStaticText> 47 | </OrangeTextWrapper> 48 | </OrangeCard> 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 | <Container> 38 | <AvatarWrapper> 39 | {imgURL ? ( 40 | <ImgWrap> 41 | <Avatar src={imgURL} /> 42 | </ImgWrap> 43 | ) : ( 44 | <UserSVG> 45 | <use href={`${sprite}#icon-gridicons_user`}></use> 46 | </UserSVG> 47 | )} 48 | 49 | <AddUserBtn> 50 | <input type="file" onChange={handleChange} /> 51 | <svg> 52 | <use href={`${sprite}#icon-check-mark`}></use> 53 | </svg> 54 | </AddUserBtn> 55 | </AvatarWrapper> 56 | 57 | <H3>{name ? name : "user"}</H3> 58 | 59 | <CardsWrap> 60 | <DailyStatsWrap> 61 | <DailyStatsCards 62 | icon="fork-and-knife" 63 | keyValue={(dailyСalories || "0") + ''} 64 | label="Daily calorie intake" 65 | fill="true" 66 | /> 67 | </DailyStatsWrap> 68 | 69 | <DailyStatsWrap> 70 | <DailyStatsCards 71 | icon="dumbbell" 72 | keyValue={(dailyTime || 0) + " min"} 73 | label="Daily norm of sports" 74 | fill="true" 75 | /> 76 | </DailyStatsWrap> 77 | </CardsWrap> 78 | 79 | <DescriptionText 80 | margin={mgForDiary} 81 | text="We understand that each individual is unique, so the entire approach to diet is relative and tailored to your unique body and goals." 82 | width={{ tablet: 439, desktop: 439 }} 83 | /> 84 | 85 | <Button 86 | type="button" 87 | onClick={() => { 88 | handleLogout(dispatch); 89 | }} 90 | > 91 | <span>Logout</span> 92 | <svg> 93 | <use href={`${sprite}#logout_`}></use> 94 | </svg> 95 | </Button> 96 | </Container> 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 | <MenuBars to="#"> 50 | <Link to={'/profile'}> 51 | <Svg> 52 | <use href={sprite + `#settings`}></use> 53 | </Svg> 54 | </Link> 55 | <AvatarWrapper> 56 | {avatarURL ? ( 57 | <UserAvatar> 58 | <img src={avatarURL} alt="user's avatar" /> 59 | </UserAvatar> 60 | ) : ( 61 | <Svg> 62 | <use href={sprite + `#user`}></use> 63 | </Svg> 64 | )} 65 | </AvatarWrapper> 66 | <ButtonMenu type="button" onClick={toggleMobMenu}> 67 | <Svg> 68 | <use href={sprite + `#menu`}></use> 69 | </Svg> 70 | </ButtonMenu> 71 | </MenuBars> 72 | 73 | {mobMenu && <Overlay onClick={toggleMobMenu}></Overlay>} 74 | <ContainerMenu className={mobMenu === true ? 'active' : ''}> 75 | <NavMenuItems> 76 | <ButtonMenuExit type="button" onClick={toggleMobMenu}> 77 | <Svg> 78 | <use href={sprite + `#close`}></use> 79 | </Svg> 80 | </ButtonMenuExit> 81 | <ContainerLink> 82 | <li onClick={toggleMobMenu}> 83 | <CustomNavLink to={'/diary'} text="Diary" /> 84 | </li> 85 | <li onClick={toggleMobMenu}> 86 | <CustomNavLink to={'/products'} text="Products" /> 87 | </li> 88 | <li onClick={toggleMobMenu}> 89 | <CustomNavLink to={'/exercises'} text="Exercises" /> 90 | </li> 91 | </ContainerLink> 92 | 93 | <NavbarToggle> 94 | <MenuBars to="#"> 95 | <ButtonMenu 96 | type="button" 97 | onClick={() => { 98 | toggleMobMenu(); 99 | handleLogout(dispatch); 100 | }} 101 | > 102 | <Span> Logout</Span> 103 | <Svg> 104 | <use href={sprite + `#logout`}></use> 105 | </Svg> 106 | </ButtonMenu> 107 | </MenuBars> 108 | </NavbarToggle> 109 | </NavMenuItems> 110 | </ContainerMenu> 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 | --------------------------------------------------------------------------------