From 39b1d38a8d397dd2596fb0deb79a829ca308c2d8 Mon Sep 17 00:00:00 2001 From: Samy Ouyahia Date: Mon, 29 Apr 2024 11:49:36 +0200 Subject: [PATCH] Created Drive App --- .github/workflows/build-drive-app.yml | 23 +++ .github/workflows/deploy-drive-app.yml | 59 ++++++ apps/drive-app/.env-cmdrc.json | 37 ++++ apps/drive-app/.eslintignore | 1 + apps/drive-app/.eslintrc.js | 14 ++ apps/drive-app/.gitignore | 26 +++ apps/drive-app/LICENSE | 32 ++++ apps/drive-app/README.md | 59 ++++++ apps/drive-app/jest.config.js | 13 ++ apps/drive-app/package.json | 106 +++++++++++ apps/drive-app/public/favicon.ico | Bin 0 -> 15086 bytes apps/drive-app/public/index.html | 20 ++ apps/drive-app/public/logo192.png | Bin 0 -> 9806 bytes apps/drive-app/public/logo512.png | Bin 0 -> 44238 bytes apps/drive-app/public/manifest.json | 25 +++ apps/drive-app/public/robots.txt | 3 + apps/drive-app/src/components/App.tsx | 23 +++ apps/drive-app/src/components/AppRouter.tsx | 43 +++++ .../src/components/AuthGuard/AuthGuard.tsx | 20 ++ .../src/components/AuthGuard/index.ts | 1 + apps/drive-app/src/components/index.ts | 3 + apps/drive-app/src/config/auth.ts | 12 ++ apps/drive-app/src/config/index.ts | 2 + apps/drive-app/src/config/sights.ts | 147 +++++++++++++++ apps/drive-app/src/i18n.ts | 26 +++ apps/drive-app/src/index.css | 14 ++ apps/drive-app/src/index.tsx | 28 +++ .../CreateInspectionPage.module.css | 12 ++ .../CreateInspectionPage.tsx | 61 ++++++ .../src/pages/CreateInspectionPage/index.ts | 1 + .../InspectionCompletePage.module.css | 9 + .../InspectionCompletePage.tsx | 8 + .../src/pages/InspectionCompletePage/index.ts | 1 + .../src/pages/LogInPage/LogInPage.module.css | 13 ++ .../src/pages/LogInPage/LogInPage.tsx | 76 ++++++++ apps/drive-app/src/pages/LogInPage/index.ts | 1 + .../PhotoCapturePage.module.css | 12 ++ .../PhotoCapturePage/PhotoCapturePage.tsx | 32 ++++ .../src/pages/PhotoCapturePage/index.ts | 1 + apps/drive-app/src/pages/index.ts | 5 + apps/drive-app/src/pages/pages.ts | 6 + apps/drive-app/src/react-app-env.d.ts | 1 + apps/drive-app/src/sentry.ts | 9 + apps/drive-app/src/setupTests.ts | 5 + apps/drive-app/src/translations/de.json | 23 +++ apps/drive-app/src/translations/en.json | 23 +++ apps/drive-app/src/translations/fr.json | 23 +++ .../test/components/AuthGuard.test.tsx | 90 +++++++++ .../test/pages/CreateInspectionPage.test.tsx | 93 +++++++++ .../pages/InspectionCompletePage.test.tsx | 16 ++ apps/drive-app/test/pages/LogInPage.test.tsx | 177 ++++++++++++++++++ .../test/pages/PhotoCapturePage.test.tsx | 71 +++++++ apps/drive-app/tsconfig.build.json | 5 + apps/drive-app/tsconfig.json | 4 + package.json | 4 + packages/network/package.json | 1 - packages/sights/package.json | 6 +- yarn.lock | 72 ++++++- 58 files changed, 1593 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build-drive-app.yml create mode 100644 .github/workflows/deploy-drive-app.yml create mode 100644 apps/drive-app/.env-cmdrc.json create mode 100644 apps/drive-app/.eslintignore create mode 100644 apps/drive-app/.eslintrc.js create mode 100644 apps/drive-app/.gitignore create mode 100644 apps/drive-app/LICENSE create mode 100644 apps/drive-app/README.md create mode 100644 apps/drive-app/jest.config.js create mode 100644 apps/drive-app/package.json create mode 100644 apps/drive-app/public/favicon.ico create mode 100644 apps/drive-app/public/index.html create mode 100644 apps/drive-app/public/logo192.png create mode 100644 apps/drive-app/public/logo512.png create mode 100644 apps/drive-app/public/manifest.json create mode 100644 apps/drive-app/public/robots.txt create mode 100644 apps/drive-app/src/components/App.tsx create mode 100644 apps/drive-app/src/components/AppRouter.tsx create mode 100644 apps/drive-app/src/components/AuthGuard/AuthGuard.tsx create mode 100644 apps/drive-app/src/components/AuthGuard/index.ts create mode 100644 apps/drive-app/src/components/index.ts create mode 100644 apps/drive-app/src/config/auth.ts create mode 100644 apps/drive-app/src/config/index.ts create mode 100644 apps/drive-app/src/config/sights.ts create mode 100644 apps/drive-app/src/i18n.ts create mode 100644 apps/drive-app/src/index.css create mode 100644 apps/drive-app/src/index.tsx create mode 100644 apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css create mode 100644 apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx create mode 100644 apps/drive-app/src/pages/CreateInspectionPage/index.ts create mode 100644 apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.module.css create mode 100644 apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.tsx create mode 100644 apps/drive-app/src/pages/InspectionCompletePage/index.ts create mode 100644 apps/drive-app/src/pages/LogInPage/LogInPage.module.css create mode 100644 apps/drive-app/src/pages/LogInPage/LogInPage.tsx create mode 100644 apps/drive-app/src/pages/LogInPage/index.ts create mode 100644 apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.module.css create mode 100644 apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx create mode 100644 apps/drive-app/src/pages/PhotoCapturePage/index.ts create mode 100644 apps/drive-app/src/pages/index.ts create mode 100644 apps/drive-app/src/pages/pages.ts create mode 100644 apps/drive-app/src/react-app-env.d.ts create mode 100644 apps/drive-app/src/sentry.ts create mode 100644 apps/drive-app/src/setupTests.ts create mode 100644 apps/drive-app/src/translations/de.json create mode 100644 apps/drive-app/src/translations/en.json create mode 100644 apps/drive-app/src/translations/fr.json create mode 100644 apps/drive-app/test/components/AuthGuard.test.tsx create mode 100644 apps/drive-app/test/pages/CreateInspectionPage.test.tsx create mode 100644 apps/drive-app/test/pages/InspectionCompletePage.test.tsx create mode 100644 apps/drive-app/test/pages/LogInPage.test.tsx create mode 100644 apps/drive-app/test/pages/PhotoCapturePage.test.tsx create mode 100644 apps/drive-app/tsconfig.build.json create mode 100644 apps/drive-app/tsconfig.json diff --git a/.github/workflows/build-drive-app.yml b/.github/workflows/build-drive-app.yml new file mode 100644 index 000000000..a4e3630f5 --- /dev/null +++ b/.github/workflows/build-drive-app.yml @@ -0,0 +1,23 @@ +name: Build Drive App +run-name: Build Drive App On Pull Request + +on: + pull_request: + branches: [main] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: 💾 Checking out the repository + uses: actions/checkout@v4 + - name: ⚙️ Setting up the MonkJs project + uses: ./.github/actions/monkjs-set-up + - name: 📱 Building the Drive app + run: yarn build:drive-app:staging diff --git a/.github/workflows/deploy-drive-app.yml b/.github/workflows/deploy-drive-app.yml new file mode 100644 index 000000000..cb7620f2d --- /dev/null +++ b/.github/workflows/deploy-drive-app.yml @@ -0,0 +1,59 @@ +name: Deploy Drive App +run-name: Deploy Drive App To Staging After Merge + +on: + push: + branches: [ main ] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: 💾 Checking out the repository + uses: actions/checkout@v4 + - name: ⚙️ Setting up the MonkJs project + uses: ./.github/actions/monkjs-set-up + - name: 📱 Building the Drive app + run: yarn build:drive-app:staging + - name: 📦 Uploading the artifact + uses: actions/upload-artifact@v4.3.1 + with: + name: build-drive-app-staging + path: apps/drive-app/build + + deploy: + name: Deploy + environment: staging + needs: + - build + container: + image: dtzar/helm-kubectl:3.14.2 + runs-on: ubuntu-latest + steps: + - name: 🔐 Authenticating to Google Cloud + uses: google-github-actions/auth@v2.1.2 + with: + credentials_json: "${{ secrets.GKE_SA_KEY }}" + - name: 🔐 Obtaining GKE credentials + uses: google-github-actions/get-gke-credentials@v2.1.0 + with: + cluster_name: ${{ secrets.GKE_CLUSTER }} + location: ${{ secrets.GKE_ZONE }} + project_id: ${{ secrets.GKE_PROJECT }} + - name: 📦 Downloading the artifact + uses: actions/download-artifact@v4.1.4 + with: + name: build-drive-app-staging + path: drive-staging + - name: 🧹 Cleaning up previous build + run: |- + kubectl -n poc exec -it $(kubectl get pods -n poc -l app.kubernetes.io/instance=poc-spa --no-headers | awk '{print $1}') -- rm -rf drive-staging + - name: 🌐 Deploying app + run: |- + kubectl -n poc cp drive-staging poc/$(kubectl get pods -n poc -l app.kubernetes.io/instance=poc-spa --no-headers | awk '{print $1}'):/app/ diff --git a/apps/drive-app/.env-cmdrc.json b/apps/drive-app/.env-cmdrc.json new file mode 100644 index 000000000..e884c01aa --- /dev/null +++ b/apps/drive-app/.env-cmdrc.json @@ -0,0 +1,37 @@ +{ + "local": { + "PORT": "17200", + "HTTPS": "true", + "ESLINT_NO_DEV_ERRORS": "true", + "REACT_APP_ENVIRONMENT": "local", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_SENTRY_DSN": "https://496e3a7f8e04df38e76d579c27c30e87@o4505669501648896.ingest.us.sentry.io/4507169054326784" + }, + "development": { + "REACT_APP_ENVIRONMENT": "development", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_SENTRY_DSN": "https://496e3a7f8e04df38e76d579c27c30e87@o4505669501648896.ingest.us.sentry.io/4507169054326784" + }, + "staging": { + "REACT_APP_ENVIRONMENT": "staging", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_SENTRY_DSN": "https://496e3a7f8e04df38e76d579c27c30e87@o4505669501648896.ingest.us.sentry.io/4507169054326784" + }, + "preview": { + "REACT_APP_ENVIRONMENT": "preview", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_SENTRY_DSN": "https://496e3a7f8e04df38e76d579c27c30e87@o4505669501648896.ingest.us.sentry.io/4507169054326784" + } +} diff --git a/apps/drive-app/.eslintignore b/apps/drive-app/.eslintignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/apps/drive-app/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/apps/drive-app/.eslintrc.js b/apps/drive-app/.eslintrc.js new file mode 100644 index 000000000..b26896911 --- /dev/null +++ b/apps/drive-app/.eslintrc.js @@ -0,0 +1,14 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: ['@monkvision/eslint-config-typescript-react'], + parserOptions: { + project: ['./tsconfig.json'], + }, + rules: { + 'import/no-extraneous-dependencies': OFF, + 'no-console': OFF, + } +} diff --git a/apps/drive-app/.gitignore b/apps/drive-app/.gitignore new file mode 100644 index 000000000..0ec2ddf7c --- /dev/null +++ b/apps/drive-app/.gitignore @@ -0,0 +1,26 @@ +# builds +build/ +lib/ +dist/ +module/ +commonjs/ +typescript/ +web-build/ + +# modules +node_modules/ +coverage/ +.expo/ +.docusaurus/ + +# logs +npm-debug.* +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# cache +.eslintcache + +# misc +.DS_Store diff --git a/apps/drive-app/LICENSE b/apps/drive-app/LICENSE new file mode 100644 index 000000000..a3592ab9e --- /dev/null +++ b/apps/drive-app/LICENSE @@ -0,0 +1,32 @@ +The Clear BSD License + +Copyright (c) [2022] [Monk](http://monk.ai) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/drive-app/README.md b/apps/drive-app/README.md new file mode 100644 index 000000000..4522d3bb6 --- /dev/null +++ b/apps/drive-app/README.md @@ -0,0 +1,59 @@ +# Drive App +This application is a custom capture app designed for Drive. + +# Running the App +In order to run the app, you will need to have [NodeJs](https://nodejs.org/en) >= 16 and +[Yarn 3](https://yarnpkg.com/getting-started/install) installed. Then, you'll need to install the required dependencies +using the following command : + +```bash +yarn install +``` + +You then need to copy the local environment configuration available in the `env.txt` file at the root of the directory +into an env file called `.env` : + +```bash +cp env.txt .env +``` + +You can then start the app by running : + +```bash +yarn start +``` + +The application is by default available at `https://localhost:17200/`. + +# Building the App +To build the app, you simply need to run the following command : + +```bash +yarn build +``` + +Don't forget to update the environment variables defined in your `.env` file for the target website. + +# Testing +## Running the Tests +To run the tests of the app, simply run the following command : + +```bash +yarn test +``` + +To run the tests as well as collecgt coverage, run the following command : + +```bash +yarn test:coverage +``` + +## Analyzing Bundle Size +After building the app using the `yarn build` command, you can analyze the bundle size using the following command : + +```bash +yarn analyze +``` + +This will open a new window on your desktop browser where you'll be able to see the sizes of each module in the final +app. diff --git a/apps/drive-app/jest.config.js b/apps/drive-app/jest.config.js new file mode 100644 index 000000000..82e82ae00 --- /dev/null +++ b/apps/drive-app/jest.config.js @@ -0,0 +1,13 @@ +const { react } = require('@monkvision/jest-config'); + +module.exports = { + ...react, + coverageThreshold: { + global: { + branches: 0, + functions: 0, + lines: 0, + statements: 0, + }, + }, +}; diff --git a/apps/drive-app/package.json b/apps/drive-app/package.json new file mode 100644 index 000000000..423210b85 --- /dev/null +++ b/apps/drive-app/package.json @@ -0,0 +1,106 @@ +{ + "name": "drive-app", + "version": "4.0.0", + "license": "BSD-3-Clause-Clear", + "packageManager": "yarn@3.2.4", + "description": "MonkJs capture app for Drive", + "author": "monkvision", + "private": true, + "scripts": { + "start": "env-cmd -e local react-scripts start", + "build:development": "env-cmd -e development react-scripts build", + "build:staging": "env-cmd -e staging react-scripts build", + "build:preview": "env-cmd -e preview react-scripts build", + "test": "jest", + "test:coverage": "jest --coverage", + "analyze": "source-map-explorer 'build/static/js/*.js'", + "eject": "react-scripts eject", + "prettier": "prettier --check ./src", + "prettier:fix": "prettier --write ./src", + "eslint": "eslint --format=pretty ./src", + "eslint:fix": "eslint --fix --format=pretty ./src", + "lint": "yarn run prettier && yarn run eslint", + "lint:fix": "yarn run prettier:fix && yarn run eslint:fix" + }, + "dependencies": { + "@auth0/auth0-react": "^2.2.4", + "@monkvision/common": "4.0.0", + "@monkvision/common-ui-web": "4.0.0", + "@monkvision/inspection-capture-web": "4.0.0", + "@monkvision/monitoring": "4.0.0", + "@monkvision/network": "4.0.0", + "@monkvision/sentry": "4.0.0", + "@monkvision/sights": "4.0.0", + "@monkvision/types": "4.0.0", + "@types/babel__core": "^7", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.18", + "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.2", + "@types/react-router-dom": "^5.3.3", + "@types/sort-by": "^1", + "axios": "^1.5.0", + "i18next": "^23.4.5", + "i18next-browser-languagedetector": "^7.1.0", + "jest-watch-typeahead": "^2.2.2", + "localforage": "^1.10.0", + "match-sorter": "^6.3.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-i18next": "^13.2.0", + "react-router-dom": "^6.22.3", + "react-scripts": "5.0.1", + "sort-by": "^1.2.0", + "source-map-explorer": "^2.5.3", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@babel/core": "^7.22.9", + "@monkvision/eslint-config-base": "4.0.0", + "@monkvision/eslint-config-typescript": "4.0.0", + "@monkvision/eslint-config-typescript-react": "4.0.0", + "@monkvision/jest-config": "4.0.0", + "@monkvision/prettier-config": "4.0.0", + "@monkvision/test-utils": "4.0.0", + "@monkvision/typescript-config": "4.0.0", + "@testing-library/dom": "^8.20.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^12.1.5", + "@typescript-eslint/eslint-plugin": "^5.43.0", + "@typescript-eslint/parser": "^5.43.0", + "env-cmd": "^10.1.0", + "eslint": "^8.29.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.5.0", + "eslint-formatter-pretty": "^4.1.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-utils": "^3.0.0", + "jest": "^29.3.1", + "prettier": "^2.7.1", + "regexpp": "^3.2.0", + "ts-jest": "^29.0.3" + }, + "prettier": "@monkvision/prettier-config", + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/apps/drive-app/public/favicon.ico b/apps/drive-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6e6a629b0a776a416796f25b11f1bcb332275af4 GIT binary patch literal 15086 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 + + + + + + + + + + React App + + + +
+ + diff --git a/apps/drive-app/public/logo192.png b/apps/drive-app/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..0690e33a3b960a7aa27253d8653a6995e1f1738e GIT binary patch literal 9806 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%0AU8{7cYv(+ebNW4*Nn<;AcV9_gk$eN zC7mU5V*~pDt3KS+#iVKT>yjJj`d&zxhG8ACScBTpySEeZs%<6>|H$K{R? zTuGV>%Ct(>KxM3Nod!tNi7zI);)us2HdUayuFch{Pa&(@q=7QpK*+N zWOOOEC!IoFWvztR6@szp4)=tF0DOB4ro1M&lXRaIdh~;vj>MI}3+XuF1WN&+2zLu6 z309Ck`xxh)<=a4cs{)UEgL{BqztmECw3s)v?is~IceQur@UDPN2V5ld8+9T*y9yTp zM?Rsp0Jp)egZssn!e?05irL%Wtb6D}nrIeqT+1LZ%ld&} zCF!(kiluCDD~c$v!So^PJ9DgSz_p;}v6yr#GOo2bX#z5LrFW%zV(kvdih#lejd(cy z1}qi`q$gRsM5G{=bgC8Y0yxnS)zfSXm5D13TFRYeeMX{XbWn1z>7f%A0X&YS9Z0${ z@FMHlaE<80cj-o4@sE%m(mB~uHtY~}1s9Xfq@ZFtKroW@0T16yk z>oY(oJcP7ch6y9|ZYgU;fZsjM o`nyxC&%$ROZ|z2=Z#QV<|MYJ!ZfNl=6aWAK07*qoM6N<$f{i}DZU6uP literal 0 HcmV?d00001 diff --git a/apps/drive-app/public/logo512.png b/apps/drive-app/public/logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..03813dd12c762c4a2b1cf2b33c3a1343af1fe14a GIT binary patch literal 44238 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 TeV;%N0zP_L|7pHgw~728)fz1- literal 0 HcmV?d00001 diff --git a/apps/drive-app/public/manifest.json b/apps/drive-app/public/manifest.json new file mode 100644 index 000000000..5634c25b3 --- /dev/null +++ b/apps/drive-app/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Monk Demo App", + "name": "Monk Inspection Demo Application", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#274B9F", + "background_color": "#202020" +} diff --git a/apps/drive-app/public/robots.txt b/apps/drive-app/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/apps/drive-app/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/drive-app/src/components/App.tsx b/apps/drive-app/src/components/App.tsx new file mode 100644 index 000000000..e660548b6 --- /dev/null +++ b/apps/drive-app/src/components/App.tsx @@ -0,0 +1,23 @@ +import { Outlet, useNavigate } from 'react-router-dom'; +import { MonkAppParamsProvider, MonkProvider, useMonkTheme } from '@monkvision/common'; +import { useTranslation } from 'react-i18next'; +import { Page } from '../pages'; + +export function App() { + const navigate = useNavigate(); + const { i18n } = useTranslation(); + const { rootStyles } = useMonkTheme(); + + return ( + navigate(Page.CREATE_INSPECTION)} + onUpdateLanguage={(lang) => i18n.changeLanguage(lang)} + > + +
+ +
+
+
+ ); +} diff --git a/apps/drive-app/src/components/AppRouter.tsx b/apps/drive-app/src/components/AppRouter.tsx new file mode 100644 index 000000000..d1d1edf98 --- /dev/null +++ b/apps/drive-app/src/components/AppRouter.tsx @@ -0,0 +1,43 @@ +import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom'; +import { + CreateInspectionPage, + InspectionCompletePage, + LogInPage, + Page, + PhotoCapturePage, +} from '../pages'; +import { AuthGuard } from './AuthGuard'; +import { App } from './App'; + +export function AppRouter() { + return ( + + + }> + } /> + } /> + } /> + + + + } + index + /> + + + + } + index + /> + } /> + + + + ); +} diff --git a/apps/drive-app/src/components/AuthGuard/AuthGuard.tsx b/apps/drive-app/src/components/AuthGuard/AuthGuard.tsx new file mode 100644 index 000000000..da36ab20e --- /dev/null +++ b/apps/drive-app/src/components/AuthGuard/AuthGuard.tsx @@ -0,0 +1,20 @@ +import { PropsWithChildren } from 'react'; +import { Navigate } from 'react-router-dom'; +import { useMonkAppParams } from '@monkvision/common'; +import { isTokenExpired, isUserAuthorized } from '@monkvision/network'; +import { Page } from '../../pages'; +import { REQUIRED_AUTHORIZATIONS } from '../../config'; + +export function AuthGuard({ children }: PropsWithChildren) { + const { authToken } = useMonkAppParams(); + + if ( + !authToken || + !isUserAuthorized(authToken, REQUIRED_AUTHORIZATIONS) || + isTokenExpired(authToken) + ) { + return ; + } + + return <>{children}; +} diff --git a/apps/drive-app/src/components/AuthGuard/index.ts b/apps/drive-app/src/components/AuthGuard/index.ts new file mode 100644 index 000000000..f9e6428b5 --- /dev/null +++ b/apps/drive-app/src/components/AuthGuard/index.ts @@ -0,0 +1 @@ +export * from './AuthGuard'; diff --git a/apps/drive-app/src/components/index.ts b/apps/drive-app/src/components/index.ts new file mode 100644 index 000000000..082a17647 --- /dev/null +++ b/apps/drive-app/src/components/index.ts @@ -0,0 +1,3 @@ +export * from './App'; +export * from './AppRouter'; +export * from './AuthGuard'; diff --git a/apps/drive-app/src/config/auth.ts b/apps/drive-app/src/config/auth.ts new file mode 100644 index 000000000..41eed9849 --- /dev/null +++ b/apps/drive-app/src/config/auth.ts @@ -0,0 +1,12 @@ +import { MonkApiPermission } from '@monkvision/network'; + +export const REQUIRED_AUTHORIZATIONS = [ + MonkApiPermission.TASK_COMPLIANCES, + MonkApiPermission.TASK_DAMAGE_DETECTION, + MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, + MonkApiPermission.TASK_WHEEL_ANALYSIS, + MonkApiPermission.INSPECTION_CREATE, + MonkApiPermission.INSPECTION_READ, + MonkApiPermission.INSPECTION_UPDATE, + MonkApiPermission.INSPECTION_WRITE, +]; diff --git a/apps/drive-app/src/config/index.ts b/apps/drive-app/src/config/index.ts new file mode 100644 index 000000000..10abf5b48 --- /dev/null +++ b/apps/drive-app/src/config/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './sights'; diff --git a/apps/drive-app/src/config/sights.ts b/apps/drive-app/src/config/sights.ts new file mode 100644 index 000000000..d61a93b2f --- /dev/null +++ b/apps/drive-app/src/config/sights.ts @@ -0,0 +1,147 @@ +import { Sight, VehicleType } from '@monkvision/types'; +import { sights } from '@monkvision/sights'; + +const APP_SIGHTS_BY_VEHICLE_TYPE: Partial> = { + [VehicleType.SUV]: [ + sights['jgc21-QIvfeg0X'], + sights['jgc21-KyUUVU2P'], + sights['jgc21-zCrDwYWE'], + sights['jgc21-z15ZdJL6'], + sights['jgc21-RE3li6rE'], + sights['jgc21-omlus7Ui'], + sights['jgc21-m2dDoMup'], + sights['jgc21-3gjMwvQG'], + sights['jgc21-ezXzTRkj'], + sights['jgc21-tbF2Ax8v'], + sights['jgc21-3JJvM7_B'], + sights['jgc21-RAVpqaE4'], + sights['jgc21-F-PPd4qN'], + sights['jgc21-XXh8GWm8'], + sights['jgc21-TRN9Des4'], + sights['jgc21-s7WDTRmE'], + sights['jgc21-__JKllz9'], + ], + [VehicleType.CROSSOVER]: [ + sights['fesc20-H1dfdfvH'], + sights['fesc20-WMUaKDp1'], + sights['fesc20-LTe3X2bg'], + sights['fesc20-WIQsf_gX'], + sights['fesc20-hp3Tk53x'], + sights['fesc20-fOt832UV'], + sights['fesc20-NLdqASzl'], + sights['fesc20-4Wqx52oU'], + sights['fesc20-dfICsfSV'], + sights['fesc20-X8k7UFGf'], + sights['fesc20-LZc7p2kK'], + sights['fesc20-5Ts1UkPT'], + sights['fesc20-gg1Xyrpu'], + sights['fesc20-P0oSEh8p'], + sights['fesc20-j3H8Z415'], + sights['fesc20-dKVLig1i'], + sights['fesc20-Wzdtgqqz'], + ], + [VehicleType.SEDAN]: [ + sights['haccord-8YjMcu0D'], + sights['haccord-DUPnw5jj'], + sights['haccord-hsCc_Nct'], + sights['haccord-GQcZz48C'], + sights['haccord-QKfhXU7o'], + sights['haccord-mdZ7optI'], + sights['haccord-bSAv3Hrj'], + sights['haccord-W-Bn3bU1'], + sights['haccord-GdWvsqrm'], + sights['haccord-ps7cWy6K'], + sights['haccord-Jq65fyD4'], + sights['haccord-OXYy5gET'], + sights['haccord-5LlCuIfL'], + sights['haccord-Gtt0JNQl'], + sights['haccord-cXSAj2ez'], + sights['haccord-KN23XXkX'], + sights['haccord-Z84erkMb'], + ], + [VehicleType.HATCHBACK]: [ + sights['ffocus18-XlfgjQb9'], + sights['ffocus18-3TiCVAaN'], + sights['ffocus18-43ljK5xC'], + sights['ffocus18-x_1SE7X-'], + sights['ffocus18-QKfhXU7o'], + sights['ffocus18-yo9eBDW6'], + sights['ffocus18-cPUyM28L'], + sights['ffocus18-S3kgFOBb'], + sights['ffocus18-9MeSIqp7'], + sights['ffocus18-X2LDjCvr'], + sights['ffocus18-jWOq2CNN'], + sights['ffocus18-P2jFq1Ea'], + sights['ffocus18-U3Bcfc2Q'], + sights['ffocus18-ts3buSD1'], + sights['ffocus18-cXSAj2ez'], + sights['ffocus18-KkeGvT-F'], + sights['ffocus18-lRDlWiwR'], + ], + [VehicleType.VAN]: [ + sights['ftransit18-wyXf7MTv'], + sights['ftransit18-UNAZWJ-r'], + sights['ftransit18-5SiNC94w'], + sights['ftransit18-Y0vPhBVF'], + sights['ftransit18-xyp1rU0h'], + sights['ftransit18-6khKhof0'], + sights['ftransit18-eXJDDYmE'], + sights['ftransit18-3Sbfx_KZ'], + sights['ftransit18-iu1Vj2Oa'], + sights['ftransit18-aA2K898S'], + sights['ftransit18-NwBMLo3Z'], + sights['ftransit18-cf0e-pcB'], + sights['ftransit18-FFP5b34o'], + sights['ftransit18-RJ2D7DNz'], + sights['ftransit18-3fnjrISV'], + sights['ftransit18-eztNpSRX'], + sights['ftransit18-TkXihCj4'], + sights['ftransit18-4NMPqEV6'], + sights['ftransit18-IIVI_pnX'], + ], + [VehicleType.MINIVAN]: [ + sights['tsienna20-YwrRNr9n'], + sights['tsienna20-HykkFbXf'], + sights['tsienna20-TI4TVvT9'], + sights['tsienna20-65mfPdRD'], + sights['tsienna20-Ia0SGJ6z'], + sights['tsienna20-1LNxhgCR'], + sights['tsienna20-U_FqYq-a'], + sights['tsienna20-670P2H2V'], + sights['tsienna20-1n_z8bYy'], + sights['tsienna20-qA3aAUUq'], + sights['tsienna20--a2RmRcs'], + sights['tsienna20-SebsoqJm'], + sights['tsienna20-u57qDaN_'], + sights['tsienna20-Rw0Gtt7O'], + sights['tsienna20-TibS83Qr'], + sights['tsienna20-cI285Gon'], + sights['tsienna20-KHB_Cd9k'], + ], + [VehicleType.PICKUP]: [ + sights['ff150-zXbg0l3z'], + sights['ff150-3he9UOwy'], + sights['ff150-KgHVkQBW'], + sights['ff150-FqbrFVr2'], + sights['ff150-g_xBOOS2'], + sights['ff150-vwE3yqdh'], + sights['ff150-V-xzfWsx'], + sights['ff150-ouGGtRnf'], + sights['ff150--xPZZd83'], + sights['ff150-nF_oFvhI'], + sights['ff150-t3KBMPeD'], + sights['ff150-3rM9XB0Z'], + sights['ff150-eOjyMInj'], + sights['ff150-18YVVN-G'], + sights['ff150-BmXfb-qD'], + sights['ff150-gFp78fQO'], + sights['ff150-7nvlys8r'], + ], +}; + +export function getSights(vehicleType: VehicleType | null): Sight[] { + return ( + APP_SIGHTS_BY_VEHICLE_TYPE[vehicleType ?? VehicleType.CROSSOVER] ?? + (APP_SIGHTS_BY_VEHICLE_TYPE[VehicleType.CROSSOVER] as Sight[]) + ); +} diff --git a/apps/drive-app/src/i18n.ts b/apps/drive-app/src/i18n.ts new file mode 100644 index 000000000..3585481f8 --- /dev/null +++ b/apps/drive-app/src/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import { monkLanguages } from '@monkvision/types'; +import en from './translations/en.json'; +import fr from './translations/fr.json'; +import de from './translations/de.json'; + +i18n + .use(I18nextBrowserLanguageDetector) + .use(initReactI18next) + .init({ + compatibilityJSON: 'v3', + fallbackLng: 'en', + interpolation: { escapeValue: false }, + supportedLngs: monkLanguages, + nonExplicitSupportedLngs: true, + resources: { + en: { translation: en }, + fr: { translation: fr }, + de: { translation: de }, + }, + }) + .catch(console.error); + +export default i18n; diff --git a/apps/drive-app/src/index.css b/apps/drive-app/src/index.css new file mode 100644 index 000000000..8b93f50df --- /dev/null +++ b/apps/drive-app/src/index.css @@ -0,0 +1,14 @@ +html, +body, +#root, +.app-container { + height: 100dvh; + width: 100%; +} + +body { + margin: 0; + background-color: #000000; + font-family: sans-serif; + color: white; +} diff --git a/apps/drive-app/src/index.tsx b/apps/drive-app/src/index.tsx new file mode 100644 index 000000000..80681b31d --- /dev/null +++ b/apps/drive-app/src/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { MonitoringProvider } from '@monkvision/monitoring'; +import { Auth0Provider } from '@auth0/auth0-react'; +import { getEnvOrThrow, MonkThemeProvider } from '@monkvision/common'; +import { sentryMonitoringAdapter } from './sentry'; +import { AppRouter } from './components'; +import './index.css'; +import './i18n'; + +ReactDOM.render( + + + + + + + , + document.getElementById('root'), +); diff --git a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css new file mode 100644 index 000000000..8b5fde5ec --- /dev/null +++ b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css @@ -0,0 +1,12 @@ +.container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.error-message { + text-align: center; + padding-bottom: 20px; +} diff --git a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx new file mode 100644 index 000000000..befea8dd7 --- /dev/null +++ b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; +import { Button, Spinner } from '@monkvision/common-ui-web'; +import { useMonkApi } from '@monkvision/network'; +import { getEnvOrThrow, useLoadingState, useMonkAppParams } from '@monkvision/common'; +import { useMonitoring } from '@monkvision/monitoring'; +import { TaskName } from '@monkvision/types'; +import { Page } from '../pages'; +import styles from './CreateInspectionPage.module.css'; + +export function CreateInspectionPage() { + const loading = useLoadingState(); + const { t } = useTranslation(); + const { handleError } = useMonitoring(); + const { authToken, inspectionId, setInspectionId } = useMonkAppParams(); + const { createInspection } = useMonkApi({ + authToken: authToken ?? '', + apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), + }); + + const handleCreateInspection = () => { + loading.start(); + createInspection({ tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }) + .then((res) => { + loading.onSuccess(); + setInspectionId(res.id); + }) + .catch((err) => { + loading.onError(err); + handleError(err); + }); + }; + + useEffect(() => { + if (!inspectionId) { + loading.start(); + handleCreateInspection(); + } + }, [inspectionId]); + + if (inspectionId) { + return ; + } + + return ( +
+ {loading.isLoading && } + {!loading.isLoading && loading.error && ( + <> +
+ {t('create-inspection.errors.create-inspection')} +
+ + + )} +
+ ); +} diff --git a/apps/drive-app/src/pages/CreateInspectionPage/index.ts b/apps/drive-app/src/pages/CreateInspectionPage/index.ts new file mode 100644 index 000000000..cd6c862e8 --- /dev/null +++ b/apps/drive-app/src/pages/CreateInspectionPage/index.ts @@ -0,0 +1 @@ +export * from './CreateInspectionPage'; diff --git a/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.module.css b/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.module.css new file mode 100644 index 000000000..0f521722d --- /dev/null +++ b/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.module.css @@ -0,0 +1,9 @@ +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 20px; +} diff --git a/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.tsx b/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.tsx new file mode 100644 index 000000000..aeebf187e --- /dev/null +++ b/apps/drive-app/src/pages/InspectionCompletePage/InspectionCompletePage.tsx @@ -0,0 +1,8 @@ +import { useTranslation } from 'react-i18next'; +import styles from './InspectionCompletePage.module.css'; + +export function InspectionCompletePage() { + const { t } = useTranslation(); + + return
{t('inspection-complete.thank-message')}
; +} diff --git a/apps/drive-app/src/pages/InspectionCompletePage/index.ts b/apps/drive-app/src/pages/InspectionCompletePage/index.ts new file mode 100644 index 000000000..8371ececb --- /dev/null +++ b/apps/drive-app/src/pages/InspectionCompletePage/index.ts @@ -0,0 +1 @@ +export * from './InspectionCompletePage'; diff --git a/apps/drive-app/src/pages/LogInPage/LogInPage.module.css b/apps/drive-app/src/pages/LogInPage/LogInPage.module.css new file mode 100644 index 000000000..a1b302ab1 --- /dev/null +++ b/apps/drive-app/src/pages/LogInPage/LogInPage.module.css @@ -0,0 +1,13 @@ +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.error-message { + text-align: center; + padding-bottom: 20px; +} diff --git a/apps/drive-app/src/pages/LogInPage/LogInPage.tsx b/apps/drive-app/src/pages/LogInPage/LogInPage.tsx new file mode 100644 index 000000000..5b5fd5cb1 --- /dev/null +++ b/apps/drive-app/src/pages/LogInPage/LogInPage.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '@monkvision/common-ui-web'; +import { isTokenExpired, isUserAuthorized, useAuth } from '@monkvision/network'; +import { useLoadingState, useMonkAppParams } from '@monkvision/common'; +import { useMonitoring } from '@monkvision/monitoring'; +import styles from './LogInPage.module.css'; +import { Page } from '../pages'; +import { REQUIRED_AUTHORIZATIONS } from '../../config'; + +function getLoginErrorMessage(err: unknown): string { + if (err instanceof Error) { + if (err.message === 'Popup closed') { + return 'login.errors.popup-closed'; + } + } + return 'login.errors.unknown'; +} + +export function LogInPage() { + const [isExpired, setIsExpired] = useState(false); + const loading = useLoadingState(); + const { authToken, inspectionId, setAuthToken } = useMonkAppParams(); + const { handleError } = useMonitoring(); + const { login, logout } = useAuth(); + const { t } = useTranslation(); + const navigate = useNavigate(); + + useEffect(() => { + if (authToken && !isUserAuthorized(authToken, REQUIRED_AUTHORIZATIONS)) { + loading.onError('login.errors.insufficient-authorization'); + } + if (authToken && isTokenExpired(authToken)) { + setIsExpired(true); + setAuthToken(null); + } + }, [authToken, loading]); + + const handleLogin = () => { + setIsExpired(false); + loading.start(); + login() + .then((token) => { + if (isUserAuthorized(token, REQUIRED_AUTHORIZATIONS)) { + loading.onSuccess(); + navigate(inspectionId ? Page.PHOTO_CAPTURE : Page.CREATE_INSPECTION); + } else { + loading.onError('login.errors.insufficient-authorization'); + } + }) + .catch((err) => { + const message = getLoginErrorMessage(err); + loading.onError(message); + handleError(err); + }); + }; + + return ( +
+ {isExpired && ( +
{t('login.errors.token-expired')}
+ )} + {loading.error &&
{t(loading.error)}
} + {authToken ? ( + + ) : ( + + )} +
+ ); +} diff --git a/apps/drive-app/src/pages/LogInPage/index.ts b/apps/drive-app/src/pages/LogInPage/index.ts new file mode 100644 index 000000000..98c073f29 --- /dev/null +++ b/apps/drive-app/src/pages/LogInPage/index.ts @@ -0,0 +1 @@ +export * from './LogInPage'; diff --git a/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.module.css b/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.module.css new file mode 100644 index 000000000..8b5fde5ec --- /dev/null +++ b/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.module.css @@ -0,0 +1,12 @@ +.container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.error-message { + text-align: center; + padding-bottom: 20px; +} diff --git a/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx b/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx new file mode 100644 index 000000000..2822dc95d --- /dev/null +++ b/apps/drive-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; +import { getEnvOrThrow, useMonkAppParams } from '@monkvision/common'; +import { DeviceOrientation } from '@monkvision/types'; +import { PhotoCapture } from '@monkvision/inspection-capture-web'; +import { useNavigate } from 'react-router-dom'; +import { getSights } from '../../config'; +import styles from './PhotoCapturePage.module.css'; +import { Page } from '../pages'; + +export function PhotoCapturePage() { + const { i18n } = useTranslation(); + const navigate = useNavigate(); + const { authToken, inspectionId, vehicleType } = useMonkAppParams({ required: true }); + + const handleComplete = () => { + navigate(Page.INSPECTION_COMPLETE); + }; + + return ( +
+ +
+ ); +} diff --git a/apps/drive-app/src/pages/PhotoCapturePage/index.ts b/apps/drive-app/src/pages/PhotoCapturePage/index.ts new file mode 100644 index 000000000..294d0d80a --- /dev/null +++ b/apps/drive-app/src/pages/PhotoCapturePage/index.ts @@ -0,0 +1 @@ +export * from './PhotoCapturePage'; diff --git a/apps/drive-app/src/pages/index.ts b/apps/drive-app/src/pages/index.ts new file mode 100644 index 000000000..2289fc93a --- /dev/null +++ b/apps/drive-app/src/pages/index.ts @@ -0,0 +1,5 @@ +export * from './pages'; +export * from './LogInPage'; +export * from './CreateInspectionPage'; +export * from './PhotoCapturePage'; +export * from './InspectionCompletePage'; diff --git a/apps/drive-app/src/pages/pages.ts b/apps/drive-app/src/pages/pages.ts new file mode 100644 index 000000000..448be3a28 --- /dev/null +++ b/apps/drive-app/src/pages/pages.ts @@ -0,0 +1,6 @@ +export enum Page { + LOG_IN = '/log-in', + CREATE_INSPECTION = '/create-inspection', + PHOTO_CAPTURE = '/photo-capture', + INSPECTION_COMPLETE = '/inspection-complete', +} diff --git a/apps/drive-app/src/react-app-env.d.ts b/apps/drive-app/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/apps/drive-app/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/drive-app/src/sentry.ts b/apps/drive-app/src/sentry.ts new file mode 100644 index 000000000..0ed36a029 --- /dev/null +++ b/apps/drive-app/src/sentry.ts @@ -0,0 +1,9 @@ +import { SentryMonitoringAdapter } from '@monkvision/sentry'; + +export const sentryMonitoringAdapter = new SentryMonitoringAdapter({ + dsn: 'https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720', + environment: 'development', + debug: true, + tracesSampleRate: 0.025, + release: '1.0', +}); diff --git a/apps/drive-app/src/setupTests.ts b/apps/drive-app/src/setupTests.ts new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/apps/drive-app/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/apps/drive-app/src/translations/de.json b/apps/drive-app/src/translations/de.json new file mode 100644 index 000000000..63276e924 --- /dev/null +++ b/apps/drive-app/src/translations/de.json @@ -0,0 +1,23 @@ +{ + "login": { + "actions": { + "log-in": "Einloggen", + "log-out": "Sich abmelden" + }, + "errors": { + "popup-closed": "Huch! Wir konnten Sie nicht anmelden, weil das Popup geschlossen wurde. Versuchen wir es noch einmal!", + "token-expired": "Ihr Authentifizierungstoken ist abgelaufen. Bitte melden Sie sich erneut an.", + "insufficient-authorization": "Sie haben nicht die erforderlichen Berechtigungen, um diese Anwendung zu nutzen. Bitte melden Sie sich ab und verwenden Sie ein anderes Konto.", + "unknown": "Huch! Beim Einloggen ist ein unerwarteter Fehler aufgetreten. Versuchen wir es noch einmal!" + } + }, + "create-inspection": { + "errors": { + "retry": "Wiederholung", + "create-inspection": "Bei der Erstellung der Inspektion ist ein unerwarteter Fehler aufgetreten." + } + }, + "inspection-complete": { + "thank-message": "Vielen Dank, dass Sie sich die Zeit genommen haben, die Inspektion durchzuführen." + } +} diff --git a/apps/drive-app/src/translations/en.json b/apps/drive-app/src/translations/en.json new file mode 100644 index 000000000..2bb81663f --- /dev/null +++ b/apps/drive-app/src/translations/en.json @@ -0,0 +1,23 @@ +{ + "login": { + "actions": { + "log-in": "Log In", + "log-out": "Log Out" + }, + "errors": { + "popup-closed": "Oops! We couldn't log you in because the popup was closed. Let's try again!", + "token-expired": "Your authentication token is expired. Please log-in again.", + "insufficient-authorization": "You do not have the required authorizations to use this application. Please log out and use a different account.", + "unknown": "Oops! An unexpected error occurred during the log in. Let's try again!" + } + }, + "create-inspection": { + "errors": { + "retry": "Retry", + "create-inspection": "An unexpected error occurred while creating the inspection." + } + }, + "inspection-complete": { + "thank-message": "Thank you for taking the time to complete the inspection." + } +} diff --git a/apps/drive-app/src/translations/fr.json b/apps/drive-app/src/translations/fr.json new file mode 100644 index 000000000..36b3ec0c7 --- /dev/null +++ b/apps/drive-app/src/translations/fr.json @@ -0,0 +1,23 @@ +{ + "login": { + "actions": { + "log-in": "Se Connecter", + "log-out": "Se Déconnecter" + }, + "errors": { + "popup-closed": "Oups ! Nous n'avons pas pu vous connecter car la pop-up s'est fermée. Essayons à nouveau !", + "token-expired": "Votre token d'authentification est expiré. Veuillez vous reconnecter.", + "insufficient-authorization": "Vous n'avez pas les autorisations nécessaires pour utiliser cette application. Veuillez vous déconnecter et utiliser un autre compte.", + "unknown": "Oups ! Une erreur inattendue est survenue lors de la connection. Essayons à nouveau !" + } + }, + "create-inspection": { + "errors": { + "retry": "Réessayer", + "create-inspection": "Une erreur inattendue est survenue lors de la création de l'inspection." + } + }, + "inspection-complete": { + "thank-message": "Merci d'avoir pris le temps pour compléter l'inspection." + } +} diff --git a/apps/drive-app/test/components/AuthGuard.test.tsx b/apps/drive-app/test/components/AuthGuard.test.tsx new file mode 100644 index 000000000..30fef078d --- /dev/null +++ b/apps/drive-app/test/components/AuthGuard.test.tsx @@ -0,0 +1,90 @@ +jest.mock('react-router-dom', () => ({ + Navigate: jest.fn(({ to, replace }) => ( +
{`Redirect to ${to}${replace ? ' with replace' : ''}`}
+ )), +})); + +import { useMonkAppParams } from '@monkvision/common'; +import { render, screen } from '@testing-library/react'; +import { isTokenExpired, isUserAuthorized, MonkApiPermission } from '@monkvision/network'; +import { AuthGuard } from '../../src/components'; +import { Page } from '../../src/pages'; + +const redirectText = `Redirect to ${Page.LOG_IN} with replace`; +const childTestId = 'child-test-id'; +const child =
; + +function expectRedirect() { + expect(screen.queryByTestId(childTestId)).toBeNull(); + expect(screen.queryByText(redirectText)).not.toBeNull(); +} + +function expectNoRedirect() { + expect(screen.queryByTestId(childTestId)).not.toBeNull(); + expect(screen.queryByText(redirectText)).toBeNull(); +} + +function mockAuthToken(params: { + defined: boolean; + authorized: boolean; + expired: boolean; +}): string | null { + const authToken = params.defined ? 'test-auth-token-test' : null; + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ authToken })); + (isUserAuthorized as jest.Mock).mockImplementation(() => params.authorized); + (isTokenExpired as jest.Mock).mockImplementation(() => params.expired); + return authToken; +} + +describe('AuthGuard component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should redirect the user if the authToken is not defined', () => { + mockAuthToken({ defined: false, authorized: true, expired: false }); + const { unmount } = render({child}); + + expectRedirect(); + + unmount(); + }); + + it('should redirect the user if the user does not have the proper authorizations', () => { + const token = mockAuthToken({ defined: true, authorized: false, expired: false }); + const { unmount } = render({child}); + + expect(isUserAuthorized).toHaveBeenCalledWith(token, [ + MonkApiPermission.TASK_COMPLIANCES, + MonkApiPermission.TASK_DAMAGE_DETECTION, + MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, + MonkApiPermission.TASK_WHEEL_ANALYSIS, + MonkApiPermission.INSPECTION_CREATE, + MonkApiPermission.INSPECTION_READ, + MonkApiPermission.INSPECTION_UPDATE, + MonkApiPermission.INSPECTION_WRITE, + ]); + expectRedirect(); + + unmount(); + }); + + it('should redirect the user if the token is expired', () => { + const token = mockAuthToken({ defined: true, authorized: true, expired: true }); + const { unmount } = render({child}); + + expect(isTokenExpired).toHaveBeenCalledWith(token); + expectRedirect(); + + unmount(); + }); + + it('should not redirect the user if the token is valid', () => { + mockAuthToken({ defined: true, authorized: true, expired: false }); + const { unmount } = render({child}); + + expectNoRedirect(); + + unmount(); + }); +}); diff --git a/apps/drive-app/test/pages/CreateInspectionPage.test.tsx b/apps/drive-app/test/pages/CreateInspectionPage.test.tsx new file mode 100644 index 000000000..051f5a844 --- /dev/null +++ b/apps/drive-app/test/pages/CreateInspectionPage.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { Navigate } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; +import { useLoadingState, useMonkAppParams } from '@monkvision/common'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { useMonkApi } from '@monkvision/network'; +import { TaskName } from '@monkvision/types'; +import { Button } from '@monkvision/common-ui-web'; +import { CreateInspectionPage, Page } from '../../src/pages'; + +const appParams = { + authToken: 'test-auth-token', + inspectionId: null, + setInspectionId: jest.fn(), +}; + +describe('CreateInspection page', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should redirect to the PhotoCapture page if the inspectionId is defined', () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ + ...appParams, + inspectionId: 'test', + })); + const { unmount } = render(); + + expectPropsOnChildMock(Navigate, { to: Page.PHOTO_CAPTURE }); + + unmount(); + }); + + it('should create the inspection and then redirect to the PhotoCapture page if the inspectionId is not defined', async () => { + const id = 'test-id-test'; + const createInspection = jest.fn(() => Promise.resolve({ id })); + (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + const { unmount } = render(); + + expect(createInspection).toHaveBeenCalledWith({ + tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS], + }); + await waitFor(() => { + expect(appParams.setInspectionId).toHaveBeenCalledWith(id); + }); + + unmount(); + }); + + it('should display an error message if the API call fails', async () => { + const createInspection = jest.fn(() => Promise.reject()); + (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + const onError = jest.fn(); + const error = 'test-error'; + (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error, start: jest.fn() })); + const { unmount } = render(); + + await waitFor(() => { + expect(appParams.setInspectionId).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalled(); + expect(screen.queryByText('create-inspection.errors.create-inspection')).not.toBeNull(); + }); + + unmount(); + }); + + it('should display a retry button if the API call fails', async () => { + const createInspection = jest.fn(() => Promise.reject()); + (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + (useLoadingState as jest.Mock).mockImplementation(() => ({ + onError: jest.fn(), + error: 'test-error', + start: jest.fn(), + })); + const { unmount } = render(); + + expectPropsOnChildMock(Button, { + variant: 'outline', + icon: 'refresh', + onClick: expect.any(Function), + }); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + + createInspection.mockClear(); + act(() => onClick()); + expect(createInspection).toHaveBeenCalled(); + + unmount(); + }); +}); diff --git a/apps/drive-app/test/pages/InspectionCompletePage.test.tsx b/apps/drive-app/test/pages/InspectionCompletePage.test.tsx new file mode 100644 index 000000000..dbe0eab15 --- /dev/null +++ b/apps/drive-app/test/pages/InspectionCompletePage.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import { useTranslation } from 'react-i18next'; +import { InspectionCompletePage } from '../../src/pages/InspectionCompletePage'; + +describe('Inspection Complete page', () => { + it('should display a thank message', () => { + const { unmount } = render(); + + expect(useTranslation).toHaveBeenCalled(); + const { t } = (useTranslation as jest.Mock).mock.results[0].value; + expect(t).toHaveBeenCalledWith('inspection-complete.thank-message'); + expect(screen.queryByText('inspection-complete.thank-message')).not.toBeNull(); + + unmount(); + }); +}); diff --git a/apps/drive-app/test/pages/LogInPage.test.tsx b/apps/drive-app/test/pages/LogInPage.test.tsx new file mode 100644 index 000000000..1ab2088e2 --- /dev/null +++ b/apps/drive-app/test/pages/LogInPage.test.tsx @@ -0,0 +1,177 @@ +import { act } from 'react-dom/test-utils'; +import { useNavigate } from 'react-router-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import { useLoadingState, useMonkAppParams } from '@monkvision/common'; +import { isTokenExpired, isUserAuthorized, useAuth } from '@monkvision/network'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { Button } from '@monkvision/common-ui-web'; +import { LogInPage, Page } from '../../src/pages'; + +const appParams = { + authToken: 'test-auth-token', + inspectionId: 'test-inspection-id', + setAuthToken: jest.fn(), +}; + +describe('Log In page', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should display a login button on the screen', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); + const { unmount } = render(); + + expectPropsOnChildMock(Button, { + onClick: expect.any(Function), + children: 'login.actions.log-in', + }); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + expect(useAuth).toHaveBeenCalled(); + const { login } = (useAuth as jest.Mock).mock.results[0].value; + + act(() => onClick()); + expect(login).toHaveBeenCalled(); + + unmount(); + }); + + it('should redirect to the PhotoCapture page after the login if the inspectionId is defined', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); + const { unmount } = render(); + + expect(Button).toHaveBeenCalled(); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + expect(useNavigate).toHaveBeenCalled(); + const navigate = (useNavigate as jest.Mock).mock.results[0].value; + + act(() => onClick()); + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith(Page.PHOTO_CAPTURE); + }); + + unmount(); + }); + + it('should redirect to the CreateInspection page after the login if the inspectionId is not defined', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ + ...appParams, + authToken: null, + inspectionId: null, + })); + const { unmount } = render(); + + expect(Button).toHaveBeenCalled(); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + expect(useNavigate).toHaveBeenCalled(); + const navigate = (useNavigate as jest.Mock).mock.results[0].value; + + act(() => onClick()); + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith(Page.CREATE_INSPECTION); + }); + + unmount(); + }); + + it('should not redirect after log in if the user does not have sufficient authorization', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); + (isUserAuthorized as jest.Mock).mockImplementation(() => false); + const onError = jest.fn(); + const error = 'test-error'; + (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error, start: jest.fn() })); + const { unmount } = render(); + + expect(Button).toHaveBeenCalled(); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + expect(useNavigate).toHaveBeenCalled(); + const navigate = (useNavigate as jest.Mock).mock.results[0].value; + + act(() => onClick()); + await waitFor(() => { + expect(onError).toHaveBeenCalledWith('login.errors.insufficient-authorization'); + expect(navigate).not.toHaveBeenCalled(); + expect(screen.queryByText(error)).not.toBeNull(); + }); + + unmount(); + }); + + it('should display an error message and a log out button if the user does not have sufficient authorizations', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + (isUserAuthorized as jest.Mock).mockImplementation(() => false); + const onError = jest.fn(); + const error = 'test-error'; + (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error })); + const { unmount } = render(); + + expect(onError).toHaveBeenCalledWith('login.errors.insufficient-authorization'); + expect(screen.queryByText(error)).not.toBeNull(); + + expectPropsOnChildMock(Button, { + primaryColor: 'alert', + onClick: expect.any(Function), + children: 'login.actions.log-out', + }); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + expect(useAuth).toHaveBeenCalled(); + const { logout } = (useAuth as jest.Mock).mock.results[0].value; + expect(logout).not.toHaveBeenCalled(); + + await act(() => onClick()); + expect(logout).toHaveBeenCalled(); + + unmount(); + }); + + it('should display an error message on the screen if the token was expired', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + (isTokenExpired as jest.Mock).mockImplementation(() => true); + const { unmount } = render(); + + expect(isTokenExpired).toHaveBeenCalledWith(appParams.authToken); + expect(appParams.setAuthToken).toHaveBeenCalledWith(null); + expect(screen.queryByText('login.errors.token-expired')).not.toBeNull(); + + unmount(); + }); + + [ + { + testCase: 'when the user closes the log in pop up', + err: new Error('Popup closed'), + label: 'login.errors.popup-closed', + }, + { + testCase: 'when an unexpected error occurrs during the log in', + err: new Error(), + label: 'login.errors.unknown', + }, + ].forEach(({ testCase, err, label }) => { + it(`should not redirect and display the proper error message ${testCase}`, async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); + (useAuth as jest.Mock).mockImplementation(() => ({ + login: jest.fn(() => Promise.reject(err)), + })); + const onError = jest.fn(); + const error = 'test-error'; + (useLoadingState as jest.Mock).mockImplementation(() => ({ + onError, + error, + start: jest.fn(), + })); + const { unmount } = render(); + + expect(Button).toHaveBeenCalled(); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + + act(() => onClick()); + await waitFor(() => { + expect(onError).toHaveBeenCalledWith(label); + expect(screen.queryByText(error)).not.toBeNull(); + }); + + unmount(); + }); + }); +}); diff --git a/apps/drive-app/test/pages/PhotoCapturePage.test.tsx b/apps/drive-app/test/pages/PhotoCapturePage.test.tsx new file mode 100644 index 000000000..6165cb169 --- /dev/null +++ b/apps/drive-app/test/pages/PhotoCapturePage.test.tsx @@ -0,0 +1,71 @@ +jest.mock('../../src/config', () => ({ + getSights: jest.fn(() => [{ id: 'test' }]), +})); + +import { render } from '@testing-library/react'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { PhotoCapture } from '@monkvision/inspection-capture-web'; +import { useMonkAppParams } from '@monkvision/common'; +import { VehicleType } from '@monkvision/types'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Page, PhotoCapturePage } from '../../src/pages'; +import { getSights } from '../../src/config'; + +const appParams = { + authToken: 'test-auth-token', + inspectionId: 'test-inspection-id', + vehicleType: '0', +}; + +describe('PhotoCapture page', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should pass the proper props to the PhotoCapture component', () => { + const language = 'test'; + (useTranslation as jest.Mock).mockImplementation(() => ({ i18n: { language } })); + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + const { unmount } = render(); + + expectPropsOnChildMock(PhotoCapture, { + apiConfig: { authToken: appParams.authToken, apiDomain: 'REACT_APP_API_DOMAIN' }, + inspectionId: appParams.inspectionId, + lang: language, + sights: expect.any(Array), + onComplete: expect.any(Function), + }); + + unmount(); + }); + + it('should use the proper sights for all vehicle types', () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + const { unmount } = render(); + + expect(getSights).toHaveBeenCalledWith(appParams.vehicleType); + expectPropsOnChildMock(PhotoCapture, { + sights: getSights(VehicleType.SUV), + }); + + unmount(); + }); + + it('should redirect to the inspection complete page after the inspection', () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + const { unmount } = render(); + + expect(useNavigate).toHaveBeenCalled(); + const navigate = (useNavigate as jest.Mock).mock.results[0].value; + expectPropsOnChildMock(PhotoCapture, { + onComplete: expect.any(Function), + }); + const { onComplete } = (PhotoCapture as unknown as jest.Mock).mock.calls[0][0]; + expect(navigate).not.toHaveBeenCalled(); + onComplete(); + expect(navigate).toHaveBeenCalledWith(Page.INSPECTION_COMPLETE); + + unmount(); + }); +}); diff --git a/apps/drive-app/tsconfig.build.json b/apps/drive-app/tsconfig.build.json new file mode 100644 index 000000000..73e2cff13 --- /dev/null +++ b/apps/drive-app/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["test"] +} diff --git a/apps/drive-app/tsconfig.json b/apps/drive-app/tsconfig.json new file mode 100644 index 000000000..60b0893a5 --- /dev/null +++ b/apps/drive-app/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@monkvision/typescript-config/tsconfig.react.json", + "include": ["src", "test"] +} diff --git a/package.json b/package.json index f47a8285d..bee1a4677 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,12 @@ "clean:all": "lerna run clean --parallel && rimraf node_modules && yarn cache clean && yarn install", "build": "lerna run build --scope '@monkvision/*'", "build:documentation": "lerna run build --scope 'monk-documentation'", + "build:demo-app:development": "lerna run build:development --scope 'monk-demo-app'", "build:demo-app:staging": "lerna run build:staging --scope 'monk-demo-app'", "build:demo-app:preview": "lerna run build:preview --scope 'monk-demo-app'", + "build:drive-app:development": "lerna run build:development --scope 'drive-app'", + "build:drive-app:staging": "lerna run build:staging --scope 'drive-app'", + "build:drive-app:preview": "lerna run build:preview --scope 'drive-app'", "deploy:documentation": "lerna run deploy --scope 'monk-documentation'", "test": "lerna run test --parallel", "test:packages": "lerna run test --parallel --scope '@monkvision/*'", diff --git a/packages/network/package.json b/packages/network/package.json index 82c6ad429..1b5cade6d 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -39,7 +39,6 @@ "react-router-dom": "^6.22.3" }, "devDependencies": { - "@monkvision/camera-web": "4.0.0", "@monkvision/eslint-config-base": "4.0.0", "@monkvision/eslint-config-typescript": "4.0.0", "@monkvision/jest-config": "4.0.0", diff --git a/packages/sights/package.json b/packages/sights/package.json index 579d3acac..6fd424363 100644 --- a/packages/sights/package.json +++ b/packages/sights/package.json @@ -16,9 +16,9 @@ "clean": "rimraf dist lib src/lib/data && mkdirp dist lib", "compile": "yarn run clean && yarn run svgo && tsc -p tsconfig.compile.json", "build": "yarn run compile && node dist/index.js && tsc -p tsconfig.build.json", - "validate": "yarn run compile && node dist/index.js --validate-only", - "test": "yarn run build && jest", - "test:coverage": "yarn run build && jest --coverage", + "validate": "node dist/index.js --validate-only", + "test": "yarn run validate && jest", + "test:coverage": "jest --coverage", "svgo": "svgo .", "prettier": "prettier --check ./src ./test ./research", "prettier:fix": "prettier --write ./src ./test ./research", diff --git a/yarn.lock b/yarn.lock index a6c9b8905..3985a6e89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3762,7 +3762,6 @@ __metadata: version: 0.0.0-use.local resolution: "@monkvision/network@workspace:packages/network" dependencies: - "@monkvision/camera-web": 4.0.0 "@monkvision/common": 4.0.0 "@monkvision/eslint-config-base": 4.0.0 "@monkvision/eslint-config-typescript": 4.0.0 @@ -9324,6 +9323,77 @@ __metadata: languageName: node linkType: hard +"drive-app@workspace:apps/drive-app": + version: 0.0.0-use.local + resolution: "drive-app@workspace:apps/drive-app" + dependencies: + "@auth0/auth0-react": ^2.2.4 + "@babel/core": ^7.22.9 + "@monkvision/common": 4.0.0 + "@monkvision/common-ui-web": 4.0.0 + "@monkvision/eslint-config-base": 4.0.0 + "@monkvision/eslint-config-typescript": 4.0.0 + "@monkvision/eslint-config-typescript-react": 4.0.0 + "@monkvision/inspection-capture-web": 4.0.0 + "@monkvision/jest-config": 4.0.0 + "@monkvision/monitoring": 4.0.0 + "@monkvision/network": 4.0.0 + "@monkvision/prettier-config": 4.0.0 + "@monkvision/sentry": 4.0.0 + "@monkvision/sights": 4.0.0 + "@monkvision/test-utils": 4.0.0 + "@monkvision/types": 4.0.0 + "@monkvision/typescript-config": 4.0.0 + "@testing-library/dom": ^8.20.0 + "@testing-library/jest-dom": ^5.16.5 + "@testing-library/react": ^12.1.5 + "@testing-library/react-hooks": ^8.0.1 + "@testing-library/user-event": ^12.1.5 + "@types/babel__core": ^7 + "@types/jest": ^27.5.2 + "@types/node": ^16.18.18 + "@types/react": ^17.0.2 + "@types/react-dom": ^17.0.2 + "@types/react-router-dom": ^5.3.3 + "@types/sort-by": ^1 + "@typescript-eslint/eslint-plugin": ^5.43.0 + "@typescript-eslint/parser": ^5.43.0 + axios: ^1.5.0 + env-cmd: ^10.1.0 + eslint: ^8.29.0 + eslint-config-airbnb-base: ^15.0.0 + eslint-config-prettier: ^8.5.0 + eslint-formatter-pretty: ^4.1.0 + eslint-plugin-eslint-comments: ^3.2.0 + eslint-plugin-import: ^2.26.0 + eslint-plugin-jest: ^25.3.0 + eslint-plugin-jsx-a11y: ^6.7.1 + eslint-plugin-prettier: ^4.2.1 + eslint-plugin-promise: ^6.1.1 + eslint-plugin-react: ^7.27.1 + eslint-plugin-react-hooks: ^4.3.0 + eslint-utils: ^3.0.0 + i18next: ^23.4.5 + i18next-browser-languagedetector: ^7.1.0 + jest: ^29.3.1 + jest-watch-typeahead: ^2.2.2 + localforage: ^1.10.0 + match-sorter: ^6.3.4 + prettier: ^2.7.1 + react: ^17.0.2 + react-dom: ^17.0.2 + react-i18next: ^13.2.0 + react-router-dom: ^6.22.3 + react-scripts: 5.0.1 + regexpp: ^3.2.0 + sort-by: ^1.2.0 + source-map-explorer: ^2.5.3 + ts-jest: ^29.0.3 + typescript: ^4.9.5 + web-vitals: ^2.1.4 + languageName: unknown + linkType: soft + "duplexer3@npm:^0.1.4": version: 0.1.5 resolution: "duplexer3@npm:0.1.5"