From 561369a7c1c7d50c409bbbe2da3705835d74cade Mon Sep 17 00:00:00 2001 From: Sven Firmbach Date: Sun, 8 Sep 2024 09:01:58 +0200 Subject: [PATCH] Update production (#675) * pushing changes to logic * clean up * update login and sign up screens * added fetch request * refactored code to use the useApi hook * refactored google login to use useLogin hook * feat: Add cursor pointer to CmButton2 component * close modals for changing password and email when successful * finished clean up for google login * made google login optional so it doesn't affect userb journey * added devmode * clean up * cleaned up * pushing changes to button * Bump @tanstack/react-query from 5.51.15 to 5.51.21 (#634) Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.51.15 to 5.51.21. - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.51.21/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump axios from 1.7.2 to 1.7.3 (#633) Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.7.3. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.3) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @mui/icons-material from 5.16.5 to 5.16.6 (#626) Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.16.5 to 5.16.6. - [Release notes](https://github.com/mui/material-ui/releases) - [Changelog](https://github.com/mui/material-ui/blob/v5.16.6/CHANGELOG.md) - [Commits](https://github.com/mui/material-ui/commits/v5.16.6/packages/mui-icons-material) --- updated-dependencies: - dependency-name: "@mui/icons-material" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump uuid and @types/uuid (#608) Bumps [uuid](https://github.com/uuidjs/uuid) and [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid). These dependencies needed to be updated together. Updates `uuid` from 9.0.1 to 10.0.0 - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v10.0.0) Updates `@types/uuid` from 9.0.8 to 10.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid) --- updated-dependencies: - dependency-name: uuid dependency-type: direct:production update-type: version-update:semver-major - dependency-name: "@types/uuid" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 630 refactor google client logic (#636) * pushing changes to logic * clean up * added fetch request * refactored code to use the useApi hook * refactored google login to use useLogin hook * finished clean up for google login * made google login optional so it doesn't affect userb journey * added devmode * clean up * cleaned up * pushing changes to button * chore: Update Google login button text capitalization --------- Co-authored-by: Svenstar74 * pushing changes * clean up * cleanup * google login code in place * pushing working code ready for refactoring * clean up credentials * pushing code so we can test backend * upgraded to @react-oauth/google so CI works * cleaned up code * clean up * clean up * clean up * changed style of button on login page to demonstrate it can be done * pushing sign up button chnages * clean up * small cleanup * chore: Clean up nginx.config file (#654) * chore: Add Content-Security-Policy header to nginx.config (#655) * chore: Update Content-Security-Policy header in nginx.config * chore: Update Content-Security-Policy header in nginx.config * chore: Update environment variables for Google OAuth * chore: Update serverdata.sh to include additional environment variable in index.html * Remove log statement * chore: Update Content-Security-Policy header in nginx.config * Update Content-Security-Policy header in nginx.config * clean up * Closes [661] - Customize the google login button (#662) * Customized the google button * login form styled * created custom google login component * cleaned and refactored * added devmode * cleanup --------- Co-authored-by: Svenstar74 * chore: Update Content-Security-Policy header in nginx.config (#664) * Closes [665] - Create userb google login and sign in (#667) * feat: Add support for logging in with Google for User B * adding changes to logic * Added logic for userb if user does not exist * feat: Update Google login component for User B journey * logic implemented for both userb sign in and login * cleanup --------- Co-authored-by: Svenstar74 * Bump @mui/material from 5.16.5 to 5.16.7 (#642) Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.16.5 to 5.16.7. - [Release notes](https://github.com/mui/material-ui/releases) - [Changelog](https://github.com/mui/material-ui/blob/v5.16.7/CHANGELOG.md) - [Commits](https://github.com/mui/material-ui/commits/v5.16.7/packages/mui-material) --- updated-dependencies: - dependency-name: "@mui/material" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump react-router-dom from 6.25.1 to 6.26.1 (#647) Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.25.1 to 6.26.1. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.26.1/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @sentry/react from 7.118.0 to 8.28.0 (#666) Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript) from 7.118.0 to 8.28.0. - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/7.118.0...8.28.0) --- updated-dependencies: - dependency-name: "@sentry/react" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @sentry/cli from 2.33.0 to 2.35.0 (#669) Bumps [@sentry/cli](https://github.com/getsentry/sentry-cli) from 2.33.0 to 2.35.0. - [Release notes](https://github.com/getsentry/sentry-cli/releases) - [Changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-cli/compare/2.33.0...2.35.0) --- updated-dependencies: - dependency-name: "@sentry/cli" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @tanstack/react-query from 5.51.21 to 5.55.0 (#670) Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.51.21 to 5.55.0. - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.55.0/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Kirstie <39728053+epixieme@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .env.development | 1 + .env.production | 1 + nginx.config | 2 +- package-lock.json | 449 ++++++++++-------- package.json | 20 +- public/logos/cm-logo.png | Bin 0 -> 8060 bytes public/logos/slogan.png | Bin 0 -> 17884 bytes serverdata.sh | 10 +- src/App.tsx | 19 +- src/api/responses.ts | 18 +- .../auth/components/ChangeEmailModal.tsx | 10 +- .../auth/components/ChangePasswordModal.tsx | 9 +- src/features/auth/components/GoogleLogin.tsx | 87 ++++ src/features/auth/components/LoginForm.tsx | 46 +- src/features/auth/components/SignUpForm.tsx | 44 +- src/features/auth/hooks/useChangeEmail.tsx | 3 + src/features/auth/hooks/useChangePassword.tsx | 3 + src/features/auth/hooks/useLogin.tsx | 102 +++- .../UserAUnauthorizedPages/LoginPage.tsx | 139 ++---- .../UserAUnauthorizedPages/SignUpPage.tsx | 31 +- src/pages/UserBPages/UserBLoginPage.tsx | 57 ++- .../UserBPages/UserBSharedSuccessPage.tsx | 25 +- .../UserBPages/UserBSharedSummaryPage.tsx | 46 +- src/pages/UserBPages/UserBSignUpPage.tsx | 58 ++- src/shared/components/CmBackButton.tsx | 6 +- src/shared/components/CmButton.tsx | 16 +- src/shared/components/CmButton2.tsx | 48 ++ src/shared/components/CmTypography.tsx | 12 +- src/shared/components/index.ts | 1 + src/shared/hooks/index.ts | 1 + src/shared/hooks/useApiClient.tsx | 18 +- src/shared/hooks/useMobileView.tsx | 21 + 32 files changed, 832 insertions(+), 471 deletions(-) create mode 100644 public/logos/cm-logo.png create mode 100644 public/logos/slogan.png create mode 100644 src/features/auth/components/GoogleLogin.tsx create mode 100644 src/shared/components/CmButton2.tsx create mode 100644 src/shared/hooks/useMobileView.tsx diff --git a/.env.development b/.env.development index 0e4200669..9e9356a70 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,3 @@ REACT_APP_API_URL=https://app-backend-test-001.azurewebsites.net REACT_APP_SENTRY_DSN=https://b0ca2fb00555461ba86f659a99cceb37@o1287611.ingest.sentry.io/6526369 +REACT_APP_GOOGLE_CLIENT_ID=40848962667-vu42kp42ba16q3mc1ah8l8rmm028jskf.apps.googleusercontent.com diff --git a/.env.production b/.env.production index 01b9acffa..ef7b8dec1 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ REACT_APP_API_URL=https://app-backend-prod-001.azurewebsites.net REACT_APP_SENTRY_DSN=https://b0ca2fb00555461ba86f659a99cceb37@o1287611.ingest.sentry.io/6526369 +REACT_APP_GOOGLE_CLIENT_ID=40848962667-vu42kp42ba16q3mc1ah8l8rmm028jskf.apps.googleusercontent.com diff --git a/nginx.config b/nginx.config index 3ae5ed512..9d32d1c87 100644 --- a/nginx.config +++ b/nginx.config @@ -9,7 +9,7 @@ server { try_files $$uri /index.html; } - add_header Content-Security-Policy "default-src 'self'; frame-src https://www.google.com/; script-src 'self' https://www.googletagmanager.com/ https://*.google-analytics.com https://*.analytics.google.com https://www.google.com/ https://www.gstatic.com/ 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://*.typekit.net https://fonts.googleapis.com; img-src * www.googletagmanager.com 'self' data: https; font-src 'self' *.typekit.net fonts.googleapis.com fonts.gstatic.com; connect-src 'self' https://*.okta.com https://app-backend-test-001.azurewebsites.net https://app-backend-prod-001.azurewebsites.net https://sentry.io https://o1287611.ingest.sentry.io/api/6526369/envelope/?sentry_key=b0ca2fb00555461ba86f659a99cceb37&sentry_version=7 https://o1287611.ingest.sentry.io/api/6526369/security/?sentry_key=b0ca2fb00555461ba86f659a99cceb37; report-uri https://o1287611.ingest.sentry.io/api/6526369/security/?sentry_key=b0ca2fb00555461ba86f659a99cceb37;"; + add_header Content-Security-Policy "default-src 'self'; frame-src https://www.google.com/ https://accounts.google.com/; script-src 'self' https://www.googletagmanager.com/ https://*.google-analytics.com https://*.analytics.google.com https://www.google.com/ https://www.gstatic.com/ https://accounts.google.com 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://*.typekit.net https://fonts.googleapis.com https://accounts.google.com; style-src-elem 'self' 'unsafe-inline' https://*.typekit.net https://fonts.googleapis.com https://accounts.google.com; img-src * www.googletagmanager.com 'self' data: https; font-src 'self' *.typekit.net fonts.googleapis.com fonts.gstatic.com; connect-src 'self' https://accounts.google.com https://accounts.google.com https://*.okta.com https://app-backend-test-001.azurewebsites.net https://app-backend-prod-001.azurewebsites.net https://sentry.io https://o1287611.ingest.sentry.io/api/6526369/envelope/?sentry_key=b0ca2fb00555461ba86f659a99cceb37&sentry_version=7 https://o1287611.ingest.sentry.io/api/6526369/security/?sentry_key=b0ca2fb00555461ba86f659a99cceb37; report-uri https://o1287611.ingest.sentry.io/api/6526369/security/?sentry_key=b0ca2fb00555461ba86f659a99cceb37;"; add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; add_header X-Content-Type-Options nosniff; diff --git a/package-lock.json b/package-lock.json index 935907b74..1e3455e35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,17 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.20", - "@mui/material": "^5.15.18", + "@mui/icons-material": "^5.16.6", + "@mui/material": "^5.16.7", + "@react-oauth/google": "^0.12.1", "@reduxjs/toolkit": "^2.2.5", - "@sentry/cli": "^2.32.1", - "@sentry/react": "^7.114.0", + "@sentry/cli": "^2.35.0", + "@sentry/react": "^8.28.0", "@sentry/tracing": "^7.114.0", - "@tanstack/react-query": "^5.49.2", - "axios": "^1.7.2", + "@tanstack/react-query": "^5.55.0", + "axios": "^1.7.3", "chart.js": "^4.4.3", + "gapi-script": "^1.2.0", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", @@ -28,9 +30,9 @@ "react-markdown": "^9.0.1", "react-query": "^3.39.3", "react-redux": "^9.1.2", - "react-router-dom": "^6.24.1", + "react-router-dom": "^6.26.1", "react-scripts": "^5.0.1", - "uuid": "^9.0.1" + "uuid": "^10.0.0" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -40,7 +42,7 @@ "@types/react-dom": "^18.3.0", "@types/react-redux": "^7.1.33", "@types/react-router-dom": "^5.3.3", - "@types/uuid": "^9.0.8", + "@types/uuid": "^10.0.0", "cypress": "^13.13.2", "eslint": "^9.6.0", "sass": "^1.77.6", @@ -4313,18 +4315,18 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.5.tgz", - "integrity": "sha512-ziFn1oPm6VjvHQcdGcAO+fXvOQEgieIj0BuSqcltFU+JXIxjPdVYNTdn2HU7/Ak5Gabk6k2u7+9PV7oZ6JT5sA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz", - "integrity": "sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.6.tgz", + "integrity": "sha512-ceNGjoXheH9wbIFa1JHmSc9QVjJUvh18KvHrR4/FkJCSi9HXJ+9ee1kUhCOEFfuxNF8UB6WWVrIUOUgRd70t0A==", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -4347,15 +4349,15 @@ } }, "node_modules/@mui/material": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.5.tgz", - "integrity": "sha512-eQrjjg4JeczXvh/+8yvJkxWIiKNHVptB/AqpsKfZBWp5mUD5U3VsjODMuUl1K2BSq0omV3CiO/mQmWSSMKSmaA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.5", - "@mui/system": "^5.16.5", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.5", + "@mui/utils": "^5.16.6", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", @@ -4391,12 +4393,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.5.tgz", - "integrity": "sha512-CSLg0YkpDqg0aXOxtjo3oTMd3XWMxvNb5d0v4AYVqwOltU8q6GvnZjhWyCLjGSCrcgfwm6/VDjaKLPlR14wxIA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.5", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" }, "engines": { @@ -4417,9 +4419,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", - "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -4448,15 +4450,15 @@ } }, "node_modules/@mui/system": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.5.tgz", - "integrity": "sha512-uzIUGdrWddUx1HPxW4+B2o4vpgKyRxGe/8BxbfXVDPNPHX75c782TseoCnR/VyfnZJfqX87GcxDmnZEE1c031g==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.5", - "@mui/styled-engine": "^5.16.4", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.5", + "@mui/utils": "^5.16.6", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -4500,9 +4502,9 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.5.tgz", - "integrity": "sha512-CwhcA9y44XwK7k2joL3Y29mRUnoBt+gOZZdGyw7YihbEwEErJYBtDwbZwVgH68zAljGe/b+Kd5bzfl63Gi3R2A==", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "^7.2.15", @@ -4661,6 +4663,15 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", @@ -4685,9 +4696,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", - "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", + "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", "engines": { "node": ">=14.0.0" } @@ -4771,68 +4782,81 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.28.0.tgz", + "integrity": "sha512-tE9++KEy8SlqibTmYymuxFVAnutsXBqrwQ936WJbjaMfkqXiro7C1El0ybkprskd0rKS7kln20Q6nQlNlMEoTA==", + "dependencies": { + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sentry-internal/feedback": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.118.0.tgz", - "integrity": "sha512-IYOGRcqIqKJJpMwBBv+0JTu0FPpXnakJYvOx/XEa/SNyF5+l7b9gGEjUVWh1ok50kTLW/XPnpnXNAGQcoKHg+w==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.28.0.tgz", + "integrity": "sha512-5vYunPCDBLCJ8QNnhepacdYheiN+UtYxpGAIaC/zjBC1nDuBgWs+TfKPo1UlO/1sesfgs9ibpxtShOweucL61g==", "dependencies": { - "@sentry/core": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { - "node": ">=12" + "node": ">=14.18" } }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.118.0.tgz", - "integrity": "sha512-XxHlCClvrxmVKpiZetFYyiBaPQNiojoBGFFVgbbWBIAPc+fWeLJ2BMoQEBjn/0NA/8u8T6lErK5YQo/eIx9+XQ==", + "node_modules/@sentry-internal/replay": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.28.0.tgz", + "integrity": "sha512-70jvzzOL5O74gahgXKyRkZgiYN93yly5gq+bbj4/6NRQ+EtPd285+ccy0laExdfyK0ugvvwD4v+1MQit52OAsg==", "dependencies": { - "@sentry/core": "7.118.0", - "@sentry/replay": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { - "node": ">=12" + "node": ">=14.18" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.118.0.tgz", - "integrity": "sha512-dERAshKlQLrBscHSarhHyUeGsu652bDTUN1FK0m4e3X48M3I5/s+0N880Qjpe5MprNLcINlaIgdQ9jkisvxjfw==", + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.28.0.tgz", + "integrity": "sha512-RfpYHDHMUKGeEdx41QtHITjEn6P3tGaDPHvatqdrD3yv4j+wbJ6laX1PrIxCpGFUtjdzkqi/KUcvUd2kzbH/FA==", "dependencies": { - "@sentry/core": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" + "@sentry-internal/replay": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/browser": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.118.0.tgz", - "integrity": "sha512-8onDOFV1VLEoBuqA5yaJeR3FF1JNuxr5C7p1oN3OwY724iTVqQnOLmZKZaSnHV3RkY67wKDGQkQIie14sc+42g==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.28.0.tgz", + "integrity": "sha512-i/gjMYzIGQiPFH1pCbdnTwH9xs9mTAqzN+goP3GWX5a58frc7h8vxyA/5z0yMd0aCW6U8mVxnoAT72vGbKbx0g==", "dependencies": { - "@sentry-internal/feedback": "7.118.0", - "@sentry-internal/replay-canvas": "7.118.0", - "@sentry-internal/tracing": "7.118.0", - "@sentry/core": "7.118.0", - "@sentry/integrations": "7.118.0", - "@sentry/replay": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry-internal/feedback": "8.28.0", + "@sentry-internal/replay": "8.28.0", + "@sentry-internal/replay-canvas": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/cli": { - "version": "2.33.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.33.0.tgz", - "integrity": "sha512-9MOzQy1UunVBhPOfEuO0JH2ofWAMmZVavTTR/Bo2CkJwI1qjyVF0UKLTXE3l4ujiJnFufOoBsVyKmYWXFerbCw==", + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.35.0.tgz", + "integrity": "sha512-7sHRJViEgHTfEXf+HD1Fb2cwmnxlILmb2NNxghP2vvrgC2PhuwuJU7AX4zg7HjJgxH9HBmnn4AJskDujaJ/6cQ==", "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", @@ -4848,23 +4872,101 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.33.0", - "@sentry/cli-linux-arm": "2.33.0", - "@sentry/cli-linux-arm64": "2.33.0", - "@sentry/cli-linux-i686": "2.33.0", - "@sentry/cli-linux-x64": "2.33.0", - "@sentry/cli-win32-i686": "2.33.0", - "@sentry/cli-win32-x64": "2.33.0" + "@sentry/cli-darwin": "2.35.0", + "@sentry/cli-linux-arm": "2.35.0", + "@sentry/cli-linux-arm64": "2.35.0", + "@sentry/cli-linux-i686": "2.35.0", + "@sentry/cli-linux-x64": "2.35.0", + "@sentry/cli-win32-i686": "2.35.0", + "@sentry/cli-win32-x64": "2.35.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.35.0.tgz", + "integrity": "sha512-dRtDaASkB1ncSbCLMIL8bxki4dPMimSdYz74XOUJ5IvDVVzEInEO7PqvyOj/cyafB+1FSNudaZ90ZRvsNN1Maw==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.33.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.0.tgz", - "integrity": "sha512-GIUKysZ1xbSklY9h1aVaLMSYLsnMSd+JuwQLR+0wKw2wJC4O5kNCPFSGikhiOZM/kvh3GO1WnXNyazFp8nLAzw==", + "node_modules/@sentry/cli-linux-arm": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.35.0.tgz", + "integrity": "sha512-zNL+/HnepZ4/MkIS8wfoUQxSa+k6r0DSSdX1TpDH5436u+3LB5rfCTBfZ624DWHKMoXX+1dI+rWSi+zL8QFMsg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.35.0.tgz", + "integrity": "sha512-NpyVz2lQWWkMa9GZkt0m4cA/wsgYnWOE6Z+4ePUGjbOIG3Ws9DLaHjYxUUYI79kxfbVCp7wLo1S6kOkj+M1Dlw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.35.0.tgz", + "integrity": "sha512-vIYwZVqx+kYZdPsenIm+UqjSCKe9Q2Aof6kzrzW0DPR1WyqIWbWG4NbiugiPTiuA1dLjUjYpGP8wyIqb8hxv4w==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.35.0.tgz", + "integrity": "sha512-7Wy5QNt6wZ8EaxEbHqP0DEiyUcXRVItRt9jzhpa2nCaawL+fwDOQCjUkHGsdIC+y14UqA+er9CaPCSp8sA6Vaw==", "cpu": [ "x64" ], "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.35.0.tgz", + "integrity": "sha512-XDcBUtO5A9elH+xgFNs6NBjkMBnz0sZLo5DU7LE77qKXULnlLeJ63eZD1ukQIRPvxEDsIEPOllRweLuAlUMDtw==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, "os": [ "win32" ], @@ -4872,62 +4974,49 @@ "node": ">=10" } }, - "node_modules/@sentry/core": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.118.0.tgz", - "integrity": "sha512-ol0xBdp3/K11IMAYSQE0FMxBOOH9hMsb/rjxXWe0hfM5c72CqYWL3ol7voPci0GELJ5CZG+9ImEU1V9r6gK64g==", - "dependencies": { - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" - }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.35.0.tgz", + "integrity": "sha512-86yHO+31qAXUeAdSCH7MNodn/cn/9xd2fTrxjtfNZWO0pX0jW91sCdomfBxhu5b977cyV9gNcqeBbc9XSIKIIA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/@sentry/integrations": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.118.0.tgz", - "integrity": "sha512-C2rR4NvIMjokF8jP5qzSf1o2zxDx7IeYnr8u15Kb2+HdZtX559owALR0hfgwnfeElqMhGlJBaKUWZ48lXJMzCQ==", + "node_modules/@sentry/core": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.28.0.tgz", + "integrity": "sha512-+If9uubvpZpvaQQw4HLiKPhrSS9/KcoA/AcdQkNm+5CVwAoOmDPtyYfkPBgfo2hLZnZQqR1bwkz/PrNoOm+gqA==", "dependencies": { - "@sentry/core": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0", - "localforage": "^1.8.1" + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/react": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.118.0.tgz", - "integrity": "sha512-oEYe5TGk8S7YzPsFqDf4xDHjfzs35/QFE+dou3S2d24OYpso8Tq4C5f1VzYmnOOyy85T7JNicYLSo0n0NSJvQg==", - "dependencies": { - "@sentry/browser": "7.118.0", - "@sentry/core": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.28.0.tgz", + "integrity": "sha512-rpeO8ikpAK7/9kVHc1IMtJc7A7IyPzswcFJ0uL1faCt8oZEzlotrQnEe6hgFnv4xvMledTrohnKj/fWVd55Aig==", + "dependencies": { + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { - "node": ">=8" + "node": ">=14.18" }, "peerDependencies": { - "react": "15.x || 16.x || 17.x || 18.x" - } - }, - "node_modules/@sentry/replay": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.118.0.tgz", - "integrity": "sha512-boQfCL+1L/tSZ9Huwi00+VtU+Ih1Lcg8HtxBuAsBCJR9pQgUL5jp7ECYdTeeHyCh/RJO7JqV1CEoGTgohe10mA==", - "dependencies": { - "@sentry-internal/tracing": "7.118.0", - "@sentry/core": "7.118.0", - "@sentry/types": "7.118.0", - "@sentry/utils": "7.118.0" - }, - "engines": { - "node": ">=12" + "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "node_modules/@sentry/tracing": { @@ -4986,22 +5075,22 @@ } }, "node_modules/@sentry/types": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.118.0.tgz", - "integrity": "sha512-2drqrD2+6kgeg+W/ycmiti3G4lJrV3hGjY9PpJ3bJeXrh6T2+LxKPzlgSEnKFaeQWkXdZ4eaUbtTXVebMjb5JA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.28.0.tgz", + "integrity": "sha512-hOfqfd92/AzBrEdMgmmV1VfOXJbIfleFTnerRl0mg/+CcNgP/6+Fdonp354TD56ouWNF2WkOM6sEKSXMWp6SEQ==", "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "7.118.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.118.0.tgz", - "integrity": "sha512-43qItc/ydxZV1Zb3Kn2M54RwL9XXFa3IAYBO8S82Qvq5YUYmU2AmJ1jgg7DabXlVSWgMA1HntwqnOV3JLaEnTQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-smhk7PJpvDMQ2DB5p2qn9UeoUHdU41IgjMmS2xklZpa8tjzBTxDeWpGvrX2fuH67D9bAJuLC/XyZjJCHLoEW5g==", "dependencies": { - "@sentry/types": "7.118.0" + "@sentry/types": "8.28.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sinclair/typebox": { @@ -5245,27 +5334,27 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.51.15", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.15.tgz", - "integrity": "sha512-xyobHDJ0yhPE3+UkSQ2/4X1fLSg7ICJI5J1JyU9yf7F3deQfEwSImCDrB1WSRrauJkMtXW7YIEcC0oA6ZZWt5A==", + "version": "5.54.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz", + "integrity": "sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.51.15", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.15.tgz", - "integrity": "sha512-UgFg23SrdIYrmfTSxAUn9g+J64VQy11pb9/EefoY/u2+zWuNMeqEOnvpJhf52XQy0yztQoyM9p6x8PFyTNaxXg==", + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.55.0.tgz", + "integrity": "sha512-2uYuxEbRQD8TORUiTUacEOwt1e8aoSqUOJFGY5TUrh6rQ3U85zrMS2wvbNhBhXGh6Vj69QDCP2yv8tIY7joo6Q==", "dependencies": { - "@tanstack/query-core": "5.51.15" + "@tanstack/query-core": "5.54.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^18.0.0" + "react": "^18 || ^19" } }, "node_modules/@tootallnate/once": { @@ -5731,9 +5820,9 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true }, "node_modules/@types/ws": { @@ -6664,9 +6753,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -10421,6 +10510,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gapi-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gapi-script/-/gapi-script-1.2.0.tgz", + "integrity": "sha512-NKTVKiIwFdkO1j1EzcrWu/Pz7gsl1GmBmgh+qhuV2Ytls04W/Eg5aiBL91SCiBM9lU0PMu7p1hTVxhh1rPT5Lw==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11182,11 +11276,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, "node_modules/immer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", @@ -15949,14 +16038,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -16018,14 +16099,6 @@ "node": ">=8.9.0" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "dependencies": { - "lie": "3.1.1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -19610,11 +19683,11 @@ } }, "node_modules/react-router": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", - "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", + "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", "dependencies": { - "@remix-run/router": "1.18.0" + "@remix-run/router": "1.19.1" }, "engines": { "node": ">=14.0.0" @@ -19624,12 +19697,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", - "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", + "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", "dependencies": { - "@remix-run/router": "1.18.0", - "react-router": "6.25.1" + "@remix-run/router": "1.19.1", + "react-router": "6.26.1" }, "engines": { "node": ">=14.0.0" @@ -22990,9 +23063,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 47cf739a0..cd0e01ac7 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,17 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.20", - "@mui/material": "^5.15.18", + "@mui/icons-material": "^5.16.6", + "@mui/material": "^5.16.7", + "@react-oauth/google": "^0.12.1", "@reduxjs/toolkit": "^2.2.5", - "@sentry/cli": "^2.32.1", - "@sentry/react": "^7.114.0", + "@sentry/cli": "^2.35.0", + "@sentry/react": "^8.28.0", "@sentry/tracing": "^7.114.0", - "@tanstack/react-query": "^5.49.2", - "axios": "^1.7.2", + "@tanstack/react-query": "^5.55.0", + "axios": "^1.7.3", "chart.js": "^4.4.3", + "gapi-script": "^1.2.0", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", @@ -38,9 +40,9 @@ "react-markdown": "^9.0.1", "react-query": "^3.39.3", "react-redux": "^9.1.2", - "react-router-dom": "^6.24.1", + "react-router-dom": "^6.26.1", "react-scripts": "^5.0.1", - "uuid": "^9.0.1" + "uuid": "^10.0.0" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -50,7 +52,7 @@ "@types/react-dom": "^18.3.0", "@types/react-redux": "^7.1.33", "@types/react-router-dom": "^5.3.3", - "@types/uuid": "^9.0.8", + "@types/uuid": "^10.0.0", "cypress": "^13.13.2", "eslint": "^9.6.0", "sass": "^1.77.6", diff --git a/public/logos/cm-logo.png b/public/logos/cm-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..15eb44e6463f9d3fc18e16c7895959fd71d4f53b GIT binary patch literal 8060 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 KMNUMnLSTYWah0(E literal 0 HcmV?d00001 diff --git a/public/logos/slogan.png b/public/logos/slogan.png new file mode 100644 index 0000000000000000000000000000000000000000..a763c0be0be87bd8dc879a612bd16545158a789b GIT binary patch literal 17884 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 enBnrzKGx~JRon7&?}3N0FnGH9xvX temp.txt && mv temp.txt $1 + +# Create JSON_STRING with both environment variables +JSON_STRING=$( jq -n --arg apiUrl "$REACT_APP_API_URL" --arg googleClientId "$REACT_APP_GOOGLE_CLIENT_ID" \ + '{REACT_APP_API_URL: $apiUrl, REACT_APP_GOOGLE_CLIENT_ID: $googleClientId}' ) + +# Replace the placeholder with the JSON_STRING in the index.html file +sed -i "s/__SERVERDATA__/$JSON_STRING/g" $INDEXPATH diff --git a/src/App.tsx b/src/App.tsx index a5529a5ba..9133dc5bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as Sentry from '@sentry/react'; +import { GoogleOAuthProvider } from '@react-oauth/google'; import './global.css'; import router from './router/Router'; @@ -12,15 +13,19 @@ import { store } from 'store/store'; export const queryClient = new QueryClient(); const App = () => { + const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID; + return ( - - - - - - - + + + + + + + + + ); }; diff --git a/src/api/responses.ts b/src/api/responses.ts index ae998bcca..015fe4b3d 100644 --- a/src/api/responses.ts +++ b/src/api/responses.ts @@ -1,6 +1,6 @@ -import { Alignment, ClimateEffect2, ClimateEffect3, Solution2, Solution3 } from "shared/types"; -import { TSharedImpact } from "types/SharedImpacts"; -import { TSharedSolution } from "types/SharedSolutions"; +import { Alignment, ClimateEffect2, ClimateEffect3, Solution2, Solution3 } from 'shared/types'; +import { TSharedImpact } from 'types/SharedImpacts'; +import { TSharedSolution } from 'types/SharedSolutions'; export type PostSession = { sessionId: string; @@ -52,6 +52,18 @@ export type Login = { }; }; +export type googleLogin = { + message: string; + access_token: string; + user: { + email: string; + first_name: string; + last_name: string; + quiz_id: string; + user_uuid: string; + }; +}; + export type CreateConversation = { conversationId: string; message: string; diff --git a/src/features/auth/components/ChangeEmailModal.tsx b/src/features/auth/components/ChangeEmailModal.tsx index d5e138fe5..f0ece6fa1 100644 --- a/src/features/auth/components/ChangeEmailModal.tsx +++ b/src/features/auth/components/ChangeEmailModal.tsx @@ -21,6 +21,14 @@ function ChangeEmailModal({ isOpen, onClose }: Props) { const emailsMatch = newEmail.value === confirmEmail.value; const formIsValid = emailValid && emailsMatch && password.value !== ''; + async function onConfirm() { + const isSuccessful = await updateEmail(newEmail.value, confirmEmail.value, password.value); + + if (isSuccessful) { + onClose(); + } + } + useEffect(() => { setNewEmail({ value: '', touched: false }); setConfirmEmail({ value: '', touched: false }); @@ -67,7 +75,7 @@ function ChangeEmailModal({ isOpen, onClose }: Props) {
- updateEmail(newEmail.value, confirmEmail.value, password.value)} disabled={!formIsValid} /> +
); diff --git a/src/features/auth/components/ChangePasswordModal.tsx b/src/features/auth/components/ChangePasswordModal.tsx index c1069b55b..8f8acfda6 100644 --- a/src/features/auth/components/ChangePasswordModal.tsx +++ b/src/features/auth/components/ChangePasswordModal.tsx @@ -18,6 +18,13 @@ function ChangePasswordModal({isOpen, onClose}: Props) { const passwordsMatch = newPassword.value === confirmPassword.value; const formIsValid = passwordValid && passwordsMatch && currentPassword.value !== ''; + async function onConfirm() { + const isSuccessful = await changePassword(currentPassword.value, newPassword.value, confirmPassword.value); + if (isSuccessful) { + onClose(); + } + } + useEffect(() => { setCurrentPassword({ value: '', touched: false }); setNewPassword({ value: '', touched: false }); @@ -64,7 +71,7 @@ function ChangePasswordModal({isOpen, onClose}: Props) {
- changePassword(currentPassword.value, newPassword.value, confirmPassword.value)} disabled={!formIsValid} +
diff --git a/src/features/auth/components/GoogleLogin.tsx b/src/features/auth/components/GoogleLogin.tsx new file mode 100644 index 000000000..630997e99 --- /dev/null +++ b/src/features/auth/components/GoogleLogin.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { CredentialResponse } from '@react-oauth/google'; + +import ROUTES from 'router/RouteConfig'; +import { CmButton2 } from 'shared/components'; +import { useLogin } from '../hooks'; +import useToastMessage from '../../../shared/hooks/useToastMessage'; + +interface Props { + navigateAfterLogin: () => void; + text?: string; +} + +function GoogleLogin({ navigateAfterLogin, text }: Props) { + const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID; + + const navigate = useNavigate(); + const location = useLocation(); + const { conversationId } = useParams(); + + const { showErrorToast } = useToastMessage(); + const { loginGoogleUserA, loginGoogleUserB } = useLogin(); + + const [isLoading, setIsLoading] = useState(false); + + async function handleGoogleSuccess(credentialResponse: CredentialResponse) { + setIsLoading(true); + try { + if (conversationId) { + const isSuccessful = await loginGoogleUserB(credentialResponse); + if (isSuccessful && location.pathname === ROUTES.USERB_LOGIN_PAGE + '/' + conversationId) { + navigate(ROUTES.USERB_CORE_VALUES_PAGE + '/' + conversationId); + } else if (isSuccessful && location.pathname === ROUTES.USERB_CORE_VALUES_PAGE + '/' + conversationId) { + } else if (!isSuccessful) { + navigate(ROUTES.USERB_HOW_CM_WORKS_PAGE + '/' + conversationId); + showErrorToast('Please Do The Quiz First'); + } else if (location.pathname === ROUTES.USERB_SIGN_UP_PAGE + '/' + conversationId) { + const isSuccessful = await loginGoogleUserA(credentialResponse); + if (isSuccessful) { + navigate(ROUTES.CLIMATE_FEED_PAGE); + } + } + } else { + const isSuccessful = await loginGoogleUserA(credentialResponse); + + if (isSuccessful) { + navigateAfterLogin(); + } else if (!isSuccessful) { + navigate(ROUTES.PRE_QUIZ_PAGE); + } + } + } catch (error) { + console.error('Error in loginGoogleUser:', error); + } + setIsLoading(false); + } + + useEffect(() => { + /* Initialize Google API client */ + (window as any).google.accounts.id.initialize({ + client_id: GOOGLE_CLIENT_ID, + callback: handleCredentialResponse, + }); + }, []); + + const handleCredentialResponse = (response: any) => { + const credential = response.credential; + // Pass the credential to your login function + handleGoogleSuccess(credential); + }; + + const handleGoogleLogin = () => { + (window as any).google.accounts.id.prompt(); // Triggers the Google sign-in prompt + }; + return ( + } + style={{ background: 'white', boxShadow: '0px 2px 3px 0px #0000002B, 0px 0px 3px 0px #00000015', border: 'none' }} + /> + ); +} + +export default GoogleLogin; diff --git a/src/features/auth/components/LoginForm.tsx b/src/features/auth/components/LoginForm.tsx index 169b534af..bd47635ef 100644 --- a/src/features/auth/components/LoginForm.tsx +++ b/src/features/auth/components/LoginForm.tsx @@ -1,14 +1,13 @@ import { useState } from 'react'; -import { CmButton, CmTextInput, CmTypography } from 'shared/components'; +import { CmButton, CmButton2, CmTextInput, CmTypography } from 'shared/components'; interface Props { isLoading: boolean; - onCancel?: () => void; onLogin: (email: string, password: string) => void; onForgotPasswordClick: () => void; } -function LoginForm({ isLoading, onCancel, onLogin, onForgotPasswordClick }: Props) { +function LoginForm({ isLoading, onLogin, onForgotPasswordClick }: Props) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -21,34 +20,18 @@ function LoginForm({ isLoading, onCancel, onLogin, onForgotPasswordClick }: Prop return (
- setEmail(e.target.value)} - placeholder='hello@climatemind.org' - type='email' - style={styles.textInput} - /> + setEmail(e.target.value)} placeholder="hello@climatemind.org" type="email" style={styles.textInput} /> - setPassword(e.target.value)} - placeholder='Super Secret Password' - type='password' - style={styles.textInput} - /> + setPassword(e.target.value)} placeholder="Super Secret Password" type="password" style={styles.textInput} />
Forgot your password? - + +
-
- {onCancel && } - +
+
); @@ -67,10 +50,17 @@ const styles: { [key: string]: React.CSSProperties } = { }, passwordResetContainer: { display: 'flex', + flexDirection: 'column', alignItems: 'center', - gap: 15, - marginTop: 10, - marginBottom: 30, + marginTop: 40, + marginBottom: '20%', + }, + resetLinkButton: { + textTransform: 'none', + textDecoration: 'underline', + letterSpacing: 0, + fontWeight: 800, + paddingTop: 0, }, }; diff --git a/src/features/auth/components/SignUpForm.tsx b/src/features/auth/components/SignUpForm.tsx index afee57f5d..c26f55a38 100644 --- a/src/features/auth/components/SignUpForm.tsx +++ b/src/features/auth/components/SignUpForm.tsx @@ -1,23 +1,18 @@ import { useState } from 'react'; -import { CmButton, CmTextInput } from 'shared/components'; -import { useAppSelector } from 'store/hooks'; +import { CmButton2, CmTextInput } from 'shared/components'; interface Props { isLoading: boolean; - onCancel?: () => void; onSignUp: (firstname: string, lastname: string, email: string, password: string) => void; } -function SignUpForm({ isLoading, onCancel, onSignUp }: Props) { - // For testing - const devMode = localStorage.getItem('devMode') === 'true'; - +function SignUpForm({ isLoading, onSignUp }: Props) { const [firstname, setFirstname] = useState({ value: '', touched: false }); const [lastname, setLastname] = useState({ value: '', touched: false }); const [email, setEmail] = useState({ value: '', touched: false }); const [password, setPassword] = useState({ value: '', touched: false }); const [confirmPassword, setConfirmPassword] = useState({ value: '', touched: false }); - const { quizId } = useAppSelector((state) => state.auth.userA); + function handleSubmit(e?: React.FormEvent) { e?.preventDefault(); if (!formIsValid) return; @@ -25,10 +20,6 @@ function SignUpForm({ isLoading, onCancel, onSignUp }: Props) { onSignUp(firstname.value, lastname.value, email.value, password.value); } - function handleGoogleAuth() { - window.location.href = `${process.env.REACT_APP_API_URL}/register/google?quizId=${quizId}`; - } - const emailValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email.value); const passwordValid = /^(?=.*[a-zA-Z])(?=.*[\d!"#$£%&'()*+,-.:;<=>?@[\]^_`{|}~]).{8,128}$/.test(password.value); const passwordsMatch = password.value === confirmPassword.value; @@ -99,33 +90,8 @@ function SignUpForm({ isLoading, onCancel, onSignUp }: Props) { style={styles.textInput} /> -
- {onCancel && } - - {devMode && } +
+
); diff --git a/src/features/auth/hooks/useChangeEmail.tsx b/src/features/auth/hooks/useChangeEmail.tsx index a1d99d5c4..351393311 100644 --- a/src/features/auth/hooks/useChangeEmail.tsx +++ b/src/features/auth/hooks/useChangeEmail.tsx @@ -10,10 +10,12 @@ function useChangeEmail() { async function updateEmail(newEmail: string, confirmEmail: string, password: string) { setIsLoading(true); + let isSuccessful = false; try { await apiClient.putEmail(newEmail, confirmEmail, password); showSuccessToast('Email updated!'); + isSuccessful = true; } catch (error) { if (error instanceof AxiosError) { console.log('Axios Error', error.response?.status) @@ -28,6 +30,7 @@ function useChangeEmail() { } setIsLoading(false); + return isSuccessful; } return { diff --git a/src/features/auth/hooks/useChangePassword.tsx b/src/features/auth/hooks/useChangePassword.tsx index f02f57c8b..cecc05225 100644 --- a/src/features/auth/hooks/useChangePassword.tsx +++ b/src/features/auth/hooks/useChangePassword.tsx @@ -9,15 +9,18 @@ function useChangePassword() { async function changePassword(currentPassword: string, newPassword: string, confirmPassword: string) { setIsLoading(true); + let isSuccessful = false; try { await apiClient.putPassword(currentPassword, newPassword, confirmPassword); showSuccessToast('Password updated!'); + isSuccessful = true; } catch (error) { showErrorToast(error.response.data.error || 'Unknown error has occurred'); } setIsLoading(false); + return isSuccessful; } return { diff --git a/src/features/auth/hooks/useLogin.tsx b/src/features/auth/hooks/useLogin.tsx index b6ff9b9bf..ca254ee8a 100644 --- a/src/features/auth/hooks/useLogin.tsx +++ b/src/features/auth/hooks/useLogin.tsx @@ -1,32 +1,94 @@ -import { useAppDispatch } from 'store/hooks'; +import { CredentialResponse } from '@react-oauth/google'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; import { useApiClient, useToastMessage } from 'shared/hooks'; import { loginUserA as loginA, loginUserB as loginB } from '../state/authSlice'; function useLogin() { const dispatch = useAppDispatch(); - + const quizIdB = useAppSelector((state) => state.auth.userB.quizId); + const quizIdA = useAppSelector((state) => state.auth.userA.quizId); + const apiClient = useApiClient(); const { showSuccessToast, showErrorToast } = useToastMessage(); /** * Login a userA, so that he can see his feeds, conversations, profile, etc. * On success we save the userA data in the store for later use. - * + * * Email and password are required. * @returns true if login was successful, false otherwise */ + async function loginGoogleUserA(response: CredentialResponse): Promise { + // const quizId = quizIdA || quizIdB; + try { + if (!response) { + throw new Error('No credential received from Google'); + } + + const data = await apiClient.postGoogleLogin(response.toString(), quizIdA); + + showSuccessToast(`Welcome back, ${data.user.first_name}!`); + const { first_name, last_name, email, quiz_id, user_uuid } = data.user; + + dispatch( + loginA({ + firstName: first_name, + lastName: last_name, + email: email, + userId: user_uuid, + quizId: quiz_id, + }) + ); + return true; + } catch (error) { + showErrorToast(error.response?.data.error ?? 'Unexpected Error. Please try again.'); + return false; + } + } + + async function loginGoogleUserB(response: CredentialResponse): Promise { + try { + if (!response) { + throw new Error('No credential received from Google'); + } + + const data = await apiClient.postGoogleLogin(response.toString(), quizIdB); + console.log('data', data); + + showSuccessToast(`Welcome back, ${data.user.first_name}!`); + const { first_name, last_name, email, quiz_id, user_uuid } = data.user; + + dispatch( + loginB({ + firstName: first_name, + lastName: last_name, + email: email, + userId: user_uuid, + quizId: quiz_id, + }) + ); + return true; + } catch (error) { + showErrorToast(error.response?.data.error ?? 'Unexpected Error. Please try again.'); + return false; + } + } + async function loginUserA(email: string, password: string): Promise { try { const data = await apiClient.postLogin(email, password); showSuccessToast(`Welcome back, ${data.user.first_name}!`); - dispatch(loginA({ - firstName: data.user.first_name, - lastName: data.user.last_name, - email: data.user.email, - userId: data.user.user_uuid, - quizId: data.user.quiz_id, - })); + dispatch( + loginA({ + firstName: data.user.first_name, + lastName: data.user.last_name, + email: data.user.email, + userId: data.user.user_uuid, + quizId: data.user.quiz_id, + }) + ); return true; } catch (error) { showErrorToast(error.response?.data.error ?? 'Unexpected Error. Please try again.'); @@ -37,7 +99,7 @@ function useLogin() { /** * Login a userB, so that he can skip the quiz in the userB journey. * On success we save the userB data in the store for later use. - * + * * Email and password are required. * @returns true if login was successful, false otherwise */ @@ -45,13 +107,15 @@ function useLogin() { try { const data = await apiClient.postLogin(email, password, false); - dispatch(loginB({ - firstName: data.user.first_name, - lastName: data.user.last_name, - email: data.user.email, - userId: data.user.user_uuid, - quizId: data.user.quiz_id, - })); + dispatch( + loginB({ + firstName: data.user.first_name, + lastName: data.user.last_name, + email: data.user.email, + userId: data.user.user_uuid, + quizId: data.user.quiz_id, + }) + ); return true; } catch (error) { @@ -60,7 +124,7 @@ function useLogin() { } } - return { loginUserA, loginUserB }; + return { loginUserA, loginUserB, loginGoogleUserA, loginGoogleUserB }; } export default useLogin; diff --git a/src/pages/UserAUnauthorizedPages/LoginPage.tsx b/src/pages/UserAUnauthorizedPages/LoginPage.tsx index bb03a2bf8..79f234e18 100644 --- a/src/pages/UserAUnauthorizedPages/LoginPage.tsx +++ b/src/pages/UserAUnauthorizedPages/LoginPage.tsx @@ -1,127 +1,84 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import GoogleLogin from 'features/auth/components/GoogleLogin'; import ROUTES from 'router/RouteConfig'; -import { CmTypography, Page, PageContent } from 'shared/components'; - -import { LoginForm, RequestPasswordResetModal, useLogin, useResetPassword, loginUserA } from 'features/auth'; -import Cookies from 'js-cookie'; -import { useAppDispatch } from 'store/hooks'; +import { CmBackButton, Page, PageContent } from 'shared/components'; +import { LoginForm, RequestPasswordResetModal, useLogin, useResetPassword } from 'features/auth'; +import { useMobileView } from 'shared/hooks'; function LoginPage() { - // For testing const devMode = localStorage.getItem('devMode') === 'true'; - const navigate = useNavigate(); const location = useLocation(); - const dispatch = useAppDispatch(); + const isMobile = useMobileView(); - // Logic for login const [isLoading, setIsLoading] = useState(false); - // const { loginUserA } = useLogin(); const { loginUserA: loginA } = useLogin(); + const { sendPasswordResetLink } = useResetPassword(); + const [showPasswordResetModal, setShowPasswordResetModal] = useState(false); async function handleSubmit(email: string, password: string) { setIsLoading(true); const isSuccessful = await loginA(email, password); if (isSuccessful) { - if (location.state && 'from' in location.state) { - navigate(location.state.from); - } else { - navigate(ROUTES.CLIMATE_FEED_PAGE); - } + navigateAfterLogin(); } - setIsLoading(false); } - // Logic for password reset - const { sendPasswordResetLink } = useResetPassword(); - const [showPasswordResetModal, setShowPasswordResetModal] = useState(false); + function navigateAfterLogin() { + if (location.state && 'from' in location.state) { + navigate(location.state.from); + } else { + navigate(ROUTES.CLIMATE_FEED_PAGE); + } + } async function handlePasswordReset(email: string) { setShowPasswordResetModal(false); await sendPasswordResetLink(email); } - // useEffect for google authentification - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const access_token = urlParams.get('access_token'); - // const refresh_token = urlParams.get('refresh_token'); - const first_name = Cookies.get('first_name'); - const last_name = Cookies.get('last_name'); - const email = Cookies.get('user_email'); - const user_id = Cookies.get('user_uuid'); - const quiz_id = Cookies.get('quiz_id'); - - console.log(first_name, last_name, email, user_id, quiz_id); - if (access_token) { - //this sets the access token to be reused in the future - Cookies.set('accessToken', access_token, { secure: true }); - console.log(first_name, last_name, email, user_id, quiz_id); - dispatch( - loginUserA({ - firstName: first_name as string, - lastName: last_name as string, - email: email as string, - quizId: quiz_id as string, - userId: user_id as string, - }) - ); - navigate(ROUTES.CLIMATE_FEED_PAGE); - } else { - console.error('No access token found'); - } - }, [location.search, dispatch]); - - const handleGoogleAuth = () => { - // Redirect to Google OAuth2 login endpoint - //need to set isloggedin to true so that the user is redirected to the climate feed page, set up a google auth redux userA slice - - window.location.href = `${process.env.REACT_APP_API_URL}/login/google`; - }; - return ( - - - Climate Mind Logo + + + {isMobile && navigate(-1)} style={styles.backButton} />} - - Climate Mind - - Sign In + Climate Mind Logo + Climate Mind Logo + +
+ setShowPasswordResetModal(true)} /> +
+ {devMode && } +
- setShowPasswordResetModal(true)} /> - {devMode && } setShowPasswordResetModal(false)} onSubmit={handlePasswordReset} />
); } +const styles: { [key: string]: React.CSSProperties } = { + backButton: { + position: 'absolute', + top: 20, + left: 10, + }, + logo: { + height: 66, + aspectRatio: 62 / 66, + objectFit: 'contain', + marginTop: '10%', + }, + slogan: { + height: 54, + aspectRatio: 234 / 54, + objectFit: 'contain', + marginTop: 16, + marginBottom: 64, + }, +}; + export default LoginPage; diff --git a/src/pages/UserAUnauthorizedPages/SignUpPage.tsx b/src/pages/UserAUnauthorizedPages/SignUpPage.tsx index c850ad437..82a21652b 100644 --- a/src/pages/UserAUnauthorizedPages/SignUpPage.tsx +++ b/src/pages/UserAUnauthorizedPages/SignUpPage.tsx @@ -1,20 +1,24 @@ import { v4 as uuidv4 } from 'uuid'; import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import ROUTES from '../../router/RouteConfig'; import { analyticsService, RegistrationPageOpenEvent } from 'services'; import { useAppSelector } from 'store/hooks'; import { CmTypography, Page, PageContent } from 'shared/components'; import { SignUpForm, useSignUp } from 'features/auth'; +import GoogleLogin from 'features/auth/components/GoogleLogin'; function SignUpPage() { + const devMode = localStorage.getItem('devMode') === 'true'; + const signUpId = uuidv4(); const navigate = useNavigate(); + const location = useLocation(); const [isLoading, setIsLoading] = useState(false); const { signUp } = useSignUp(); - const { sessionId, quizId } = useAppSelector(state => state.auth.userA); + const { sessionId, quizId } = useAppSelector((state) => state.auth.userA); async function signUpHandler(firstname: string, lastname: string, email: string, password: string) { setIsLoading(true); @@ -22,20 +26,33 @@ function SignUpPage() { if (success) navigate(ROUTES.CLIMATE_FEED_PAGE); setIsLoading(false); } - + function navigateAfterLogin() { + if (location.state && 'from' in location.state) { + navigate(location.state.from); + } else { + navigate(ROUTES.CLIMATE_FEED_PAGE); + } + } useEffect(() => { if (sessionId) analyticsService.postEvent(RegistrationPageOpenEvent, signUpId); }, [sessionId, signUpId]); return ( - + - Create a Climate Mind account - + + Create a Climate Mind account + + + Save your results, see your climate topics, and start talking. - +
+ +
+ {devMode && } +
); diff --git a/src/pages/UserBPages/UserBLoginPage.tsx b/src/pages/UserBPages/UserBLoginPage.tsx index c42bc25ce..77d6aa5b0 100644 --- a/src/pages/UserBPages/UserBLoginPage.tsx +++ b/src/pages/UserBPages/UserBLoginPage.tsx @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import ROUTES from 'router/RouteConfig'; -import { CmTypography, Page, PageContent } from 'shared/components'; +import { CmBackButton, Page, PageContent } from 'shared/components'; import { LoginForm, RequestPasswordResetModal, useLogin, useResetPassword } from 'features/auth'; +import { useMobileView } from 'shared/hooks'; +import GoogleLogin from 'features/auth/components/GoogleLogin'; function UserBLoginPage() { const navigate = useNavigate(); const { conversationId } = useParams(); + const isMobile = useMobileView(); // Logic for login const [isLoading, setIsLoading] = useState(false); const { loginUserB } = useLogin(); + const location = useLocation(); async function handleSubmit(email: string, password: string) { setIsLoading(true); @@ -20,6 +24,14 @@ function UserBLoginPage() { setIsLoading(false); } + function navigateAfterLogin() { + if (location.state && 'from' in location.state) { + navigate(location.state.from); + } else { + navigate(ROUTES.CLIMATE_FEED_PAGE); + } + } + // Logic for password reset const { sendPasswordResetLink } = useResetPassword(); const [showPasswordResetModal, setShowPasswordResetModal] = useState(false); @@ -27,22 +39,45 @@ function UserBLoginPage() { async function handlePasswordReset(email: string) { setShowPasswordResetModal(false); await sendPasswordResetLink(email); - }; + } return ( - - - Climate Mind Logo - - Climate Mind - Sign In - - navigate(-1)} onForgotPasswordClick={() => setShowPasswordResetModal(true)} /> + + + {isMobile && navigate(-1)} style={styles.backButton} />} + Climate Mind Logo + Climate Mind Logo +
+ setShowPasswordResetModal(true)} /> +
+ +
setShowPasswordResetModal(false)} onSubmit={handlePasswordReset} />
); } +const styles: { [key: string]: React.CSSProperties } = { + backButton: { + position: 'absolute', + top: 20, + left: 10, + }, + logo: { + height: 66, + aspectRatio: 62 / 66, + objectFit: 'contain', + marginTop: '25%', + }, + slogan: { + height: 54, + aspectRatio: 234 / 54, + objectFit: 'contain', + marginTop: 16, + marginBottom: 64, + }, +}; + export default UserBLoginPage; diff --git a/src/pages/UserBPages/UserBSharedSuccessPage.tsx b/src/pages/UserBPages/UserBSharedSuccessPage.tsx index cf84e5646..b51070187 100644 --- a/src/pages/UserBPages/UserBSharedSuccessPage.tsx +++ b/src/pages/UserBPages/UserBSharedSuccessPage.tsx @@ -1,3 +1,4 @@ +import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import CloudDoneIcon from '@mui/icons-material/CloudDone'; @@ -7,8 +8,10 @@ import { capitalize } from 'helpers/capitalize'; import { CmButton, CmTypography, Page, PageContent } from 'shared/components'; import { FooterAppBar } from 'features/userB/components'; import { useConversation } from 'features/conversations'; +import { RootState } from 'store/store'; function UserBSharedSuccessPage() { + const isUserBLoggedIn = useSelector((state: RootState) => state.auth.userB.isLoggedIn); const navigate = useNavigate(); const { conversationId } = useParams(); @@ -29,20 +32,13 @@ function UserBSharedSuccessPage() { return ( - Shared! + Shared! - {conversation && - {capitalize(conversation.userA.name)} can now see which values, - impacts, and solutions you have in common and will be in touch soon! - } + {conversation && {capitalize(conversation.userA.name)} can now see which values, impacts, and solutions you have in common and will be in touch soon!} - + - + Until then, why not create your own account? @@ -74,12 +70,7 @@ function UserBSharedSuccessPage() { /> )} - handleCreateAccount()} - style={{ margin: 'auto' }} - /> + {!isUserBLoggedIn && handleCreateAccount()} style={{ margin: 'auto' }} />} ); diff --git a/src/pages/UserBPages/UserBSharedSummaryPage.tsx b/src/pages/UserBPages/UserBSharedSummaryPage.tsx index 89f069a5f..cdde6aa51 100644 --- a/src/pages/UserBPages/UserBSharedSummaryPage.tsx +++ b/src/pages/UserBPages/UserBSharedSummaryPage.tsx @@ -1,3 +1,4 @@ +import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import ROUTES_CONFIG from '../../router/RouteConfig'; @@ -10,6 +11,7 @@ import { useAppSelector } from 'store/hooks'; import { RootState } from 'store/store'; function UserBSharedSummaryPage() { + const isUserBLoggedIn = useSelector((state: RootState) => state.auth.userB.isLoggedIn); const navigate = useNavigate(); const { conversationId } = useParams(); @@ -38,30 +40,36 @@ function UserBSharedSummaryPage() { return ( - {conversation && !conversation.consent && (<> - Sharing is caring! - Share the impact and solutions you selected with {capitalizeFirstLetter(conversation.userA.name)} and let them know which core values you share! - )} + {conversation && !conversation.consent && ( + <> + Sharing is caring! + Share the impact and solutions you selected with {capitalizeFirstLetter(conversation.userA.name)} and let them know which core values you share! + + )} - {conversation && conversation.consent && (<> - Share Summary - Here are the topics you shared with {capitalizeFirstLetter(conversation.userA.name)}. - )} + {conversation && conversation.consent && ( + <> + Share Summary + Here are the topics you shared with {capitalizeFirstLetter(conversation.userA.name)}. + + )} {isLoading && } - {alignmentSummary.data && ( - + {alignmentSummary.data && } + + {selectedTopics.data && ( + <> + {selectedTopics.data.climateEffects && } + {selectedTopics.data.climateSolutions && ( + <> + + + + )} + )} - {selectedTopics.data && (<> - {selectedTopics.data.climateEffects && } - {selectedTopics.data.climateSolutions && <> - - - } - )} - {conversation && !conversation.consent && ( We only share your matching core values, selected impact and solutions with {capitalizeFirstLetter(conversation.userA.name)}. No other information, in case you were wondering. :) @@ -73,7 +81,7 @@ function UserBSharedSummaryPage() { {!conversation.consent && handleNotNow()} style={{ backgroundColor: 'transparent', borderColor: 'black' }} />} {!conversation.consent && handleShareWithUserA()} />} - {conversation.consent && handleCreateAccount()} style={{ margin: 'auto' }} />} + {!isUserBLoggedIn && conversation.consent && handleCreateAccount()} style={{ margin: 'auto' }} />} )} diff --git a/src/pages/UserBPages/UserBSignUpPage.tsx b/src/pages/UserBPages/UserBSignUpPage.tsx index ddb13c7ae..7a88466fb 100644 --- a/src/pages/UserBPages/UserBSignUpPage.tsx +++ b/src/pages/UserBPages/UserBSignUpPage.tsx @@ -1,20 +1,25 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import ROUTES from '../../router/RouteConfig'; import { RegistrationPageOpenEvent, analyticsService } from 'services'; import { useAppSelector } from 'store/hooks'; -import { CmTypography, Page, PageContent } from 'shared/components'; +import { CmBackButton, CmButton, CmTypography, Page, PageContent } from 'shared/components'; import { SignUpForm, useSignUp } from 'features/auth'; +import { useMobileView } from 'shared/hooks'; +import GoogleLogin from 'features/auth/components/GoogleLogin'; function UserBSignUpPage() { const signUpId = uuidv4(); + const navigate = useNavigate(); + const location = useLocation(); + const isMobile = useMobileView(); - const [isLoading, setIsLoading] = useState(false); + const { sessionId, quizId } = useAppSelector((state) => state.auth.userB); const { signUp } = useSignUp(); - const { sessionId, quizId } = useAppSelector(state => state.auth.userB); + const [isLoading, setIsLoading] = useState(false); async function signUpHandler(firstName: string, lastName: string, email: string, password: string) { setIsLoading(true); @@ -23,24 +28,51 @@ function UserBSignUpPage() { setIsLoading(false); } + function navigateAfterLogin() { + if (location.state && 'from' in location.state) { + navigate(location.state.from); + } else { + navigate(ROUTES.CLIMATE_FEED_PAGE); + } + } useEffect(() => { if (sessionId) analyticsService.postEvent(RegistrationPageOpenEvent, signUpId); }, [signUpId, sessionId]); return ( - - - Create a Climate Mind account + + + {isMobile && navigate(-1)} style={styles.backButton} />} - navigate(-1)} /> + Welcome to Climate Mind - - By creating an account you can access your core values and Climate Feed on any computer. - You can share the core values quiz with other friends and see how you relate. - +
+ Already have an account? + navigate(ROUTES.LOGIN_PAGE)} style={styles.loginButton} /> +
+
+ +
+ +
); } +const styles: { [key: string]: React.CSSProperties } = { + backButton: { + position: 'absolute', + top: 20, + left: 10, + }, + loginButton: { + textTransform: 'none', + textDecoration: 'underline', + letterSpacing: 0, + fontWeight: 800, + paddingTop: 0, + }, +}; + export default UserBSignUpPage; diff --git a/src/shared/components/CmBackButton.tsx b/src/shared/components/CmBackButton.tsx index 2ff1b48a5..2b6091b46 100644 --- a/src/shared/components/CmBackButton.tsx +++ b/src/shared/components/CmBackButton.tsx @@ -9,9 +9,9 @@ interface Props { function CmBackButton({ text = 'Back', onClick, style }: Props) { return ( -
- - {text} +
+ + {text}
); } diff --git a/src/shared/components/CmButton.tsx b/src/shared/components/CmButton.tsx index 60aee4870..c2a9a403d 100644 --- a/src/shared/components/CmButton.tsx +++ b/src/shared/components/CmButton.tsx @@ -10,6 +10,7 @@ interface Props extends React.ButtonHTMLAttributes { style?: React.CSSProperties; startIcon?: React.ReactNode; isLoading?: boolean; + children?: React.ReactNode; } function CmButton({ text, onClick, color = 'success', variant = 'outlined', isLoading, style, startIcon, ...rest }: Props) { @@ -21,18 +22,18 @@ function CmButton({ text, onClick, color = 'success', variant = 'outlined', isLo style={{ padding: 5, marginLeft: startIcon ? 10 : 0, - color: (rest.disabled || isLoading) ? '#77AAAF' : 'black', + color: rest.disabled || isLoading ? '#77AAAF' : 'black', cursor: rest.disabled ? 'default' : 'pointer', visibility: isLoading ? 'hidden' : 'visible', + ...style, }} onClick={rest.disabled ? () => {} : onClick} > {text} -
- {isLoading && } -
+ +
{isLoading && }
); } @@ -40,13 +41,14 @@ function CmButton({ text, onClick, color = 'success', variant = 'outlined', isLo const borderColor = color === 'error' ? 'red' : color === 'userb' ? '#a347ff' : '#39f5ad'; return ( - + ); +} + +const styles: { [key: string]: React.CSSProperties } = { + button: { + borderRadius: 100, + borderWidth: 1, + borderColor: '#07373B', + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 36, + paddingRight: 29, + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + alignSelf: 'center', + minWidth: 240, + cursor: 'pointer', + }, +}; + +export default CmButton2; diff --git a/src/shared/components/CmTypography.tsx b/src/shared/components/CmTypography.tsx index bc620b369..6c8ca8e7d 100644 --- a/src/shared/components/CmTypography.tsx +++ b/src/shared/components/CmTypography.tsx @@ -1,5 +1,5 @@ interface Props extends React.HTMLAttributes { - variant: 'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'body-italics' | 'button' | 'caption' | 'label' | 'overline'; + variant: 'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'body-italics' | 'button' | 'onboarding-button' | 'caption' | 'label' | 'overline'; children: React.ReactNode; style?: React.CSSProperties; } @@ -29,6 +29,9 @@ function CmTypography({ variant, children, style, ...rest }: Props) { case 'button': textStyle = styles.button; break; + case 'onboarding-button': + textStyle = styles.onboardingButton; + break; case 'caption': textStyle = styles.caption; break; @@ -110,6 +113,13 @@ const styles: { [key: string]: React.CSSProperties } = { textAlign: 'center', textTransform: 'uppercase', }, + onboardingButton: { + color: '#07373B', + fontFamily: "'Nunito', Arial, sans-serif", + fontSize: 16, + textAlign: 'center', + fontWeight: 'bold', + }, caption: { color: '#07373B', fontFamily: "'Nunito', Arial, sans-serif", diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts index e2f541ac9..5187c4a44 100644 --- a/src/shared/components/index.ts +++ b/src/shared/components/index.ts @@ -1,5 +1,6 @@ export { default as CmTypography } from './CmTypography'; export { default as CmButton } from './CmButton'; +export { default as CmButton2 } from './CmButton2'; export { default as CmBackButton } from './CmBackButton'; export { default as CmCarousel } from './CmCarousel'; export { default as CmTextInput } from './CmTextInput'; diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index 3afe09d81..658a61862 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -1,2 +1,3 @@ export { default as useApiClient } from './useApiClient'; export { default as useToastMessage } from './useToastMessage'; +export { default as useMobileView } from './useMobileView'; diff --git a/src/shared/hooks/useApiClient.tsx b/src/shared/hooks/useApiClient.tsx index 8da005b7f..fe993d4d4 100644 --- a/src/shared/hooks/useApiClient.tsx +++ b/src/shared/hooks/useApiClient.tsx @@ -26,12 +26,11 @@ const validateToken = (token: string): boolean => { function useApiClient() { const { showErrorToast } = useToastMessage(); - // const { logoutUserA } = useLogout(); const sessionId = useAppSelector((state) => state.auth.userA.sessionId); const quizId = useAppSelector((state) => state.auth.userA.quizId); - async function apiCall(method: string, endpoint: string, headers: { [key: string]: string }, data?: any) { + async function apiCall(method: string, endpoint: string, headers: { [key: string]: string }, data?: any, withCredentials?: boolean) { // Add sessionId to headers if (sessionId) { headers['X-Session-Id'] = sessionId; @@ -54,6 +53,7 @@ function useApiClient() { method, headers, data, + withCredentials, }); return response; @@ -148,11 +148,22 @@ function useApiClient() { return response.data; } + async function postGoogleLogin(credential: string, quizId: string) { + if (quizId) { + const response = await apiCall('post', '/auth/google', {}, { credential, quizId }, true); + return response.data; + } + + const response = await apiCall('post', '/auth/google', {}, { credential }, true); + const { access_token } = response.data; + Cookies.set('accessToken', access_token, { secure: true, sameSite: 'strict' }); + return response.data; + } + async function postLogout() { // Remove the tokens from cookies Cookies.remove('accessToken'); Cookies.remove('refreshToken'); - await apiCall('post', '/logout', {}); } @@ -447,6 +458,7 @@ function useApiClient() { postRegister, deleteAccount, postLogin, + postGoogleLogin, postLogout, postRefresh, checkPasswordResetLink, diff --git a/src/shared/hooks/useMobileView.tsx b/src/shared/hooks/useMobileView.tsx new file mode 100644 index 000000000..20060c2e0 --- /dev/null +++ b/src/shared/hooks/useMobileView.tsx @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; + +const useMobileView = () => { + const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth <= 768); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return isMobile; +}; + +export default useMobileView;