From f85755baa65dac3467d8a51cf7c6650bc934c597 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 9 Feb 2026 14:01:24 +0100 Subject: [PATCH 1/3] feat: ignore seeds and drizzle auto generated files --- .prettierignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.prettierignore b/.prettierignore index 5b26b8f9..e8cf6e98 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,5 @@ node_modules /postgres-data /app/styles/**/*.css +/app/db/seeds/**/* +/app/db/drizzle/**/* \ No newline at end of file From 06ae341d6d0d96b73dcaa7526a5002a7551d798a Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 9 Feb 2026 14:06:40 +0100 Subject: [PATCH 2/3] fix: correct prettierignore --- .prettierignore | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.prettierignore b/.prettierignore index e8cf6e98..60bf1ac6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,7 +5,5 @@ node_modules .env /postgres-data - -/app/styles/**/*.css -/app/db/seeds/**/* -/app/db/drizzle/**/* \ No newline at end of file +/db/seeds/**/* +/drizzle/**/* \ No newline at end of file From de7d1eef5d69db3d31cf670042b87417bccfb455 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 9 Feb 2026 14:06:49 +0100 Subject: [PATCH 3/3] feat: run prettier --- .github/FUNDING.yml | 4 +- .github/dependabot.yaml | 18 +- .github/workflows/deploy.yml | 2 +- .github/workflows/purge-pr.yml | 2 +- .github/workflows/purge.yml | 4 +- README.md | 111 +- app/components/aggregation-filter.tsx | 146 +- app/components/client-only.tsx | 22 +- app/components/color-picker.tsx | 142 +- app/components/daterange-filter.tsx | 310 +- app/components/device-card.tsx | 56 +- .../device-detail/device-detail-box.tsx | 6 +- app/components/device-detail/entry-logs.tsx | 306 +- app/components/device-detail/graph.tsx | 9 +- .../device-detail/profile-box-selection.tsx | 124 +- app/components/device-detail/share-link.tsx | 190 +- app/components/device/new/advanced-info.tsx | 482 +- .../device/new/custom-device-config.tsx | 212 +- app/components/device/new/device-info.tsx | 346 +- app/components/device/new/sensors-info.tsx | 417 +- app/components/device/new/summary-info.tsx | 161 +- app/components/error-boundary.tsx | 67 +- app/components/error-message.tsx | 48 +- app/components/header/download.tsx | 696 +- app/components/header/home/index.tsx | 38 +- app/components/header/index.tsx | 42 +- app/components/header/menu/index.tsx | 426 +- .../header/menu/my-devices/index.tsx | 50 +- app/components/header/menu/profile/index.tsx | 50 +- .../header/menu/user-settings/index.tsx | 50 +- .../nav-bar/filter-options/filter-options.tsx | 328 +- .../nav-bar/filter-options/filter-tags.tsx | 236 +- app/components/header/nav-bar/index.tsx | 208 +- .../header/nav-bar/nav-bar-handler.tsx | 134 +- .../phenomenon-select/phenomenon-select.tsx | 97 +- .../header/nav-bar/time-filter/index.tsx | 799 +- .../nav-bar/time-filter/time-filter.tsx | 673 +- .../header/nav-bar/use-keyboard-nav.tsx | 118 +- app/components/header/notification/index.tsx | 83 +- app/components/label-button.tsx | 6 +- app/components/landing/globe.client.tsx | 92 +- app/components/landing/header/header.tsx | 246 +- app/components/landing/sections/connect.tsx | 134 +- .../landing/sections/features-card.tsx | 30 +- .../landing/sections/features-carousel.tsx | 334 +- app/components/landing/sections/features.tsx | 141 +- .../landing/sections/integrations.tsx | 109 +- app/components/landing/sections/partners.tsx | 136 +- .../landing/sections/pricing-plans.tsx | 82 +- app/components/landing/sections/tools.tsx | 178 +- app/components/landing/stats.tsx | 112 +- app/components/map/filter-visualization.tsx | 202 +- app/components/map/geocoder-control.tsx | 233 +- app/components/map/index.ts | 4 +- .../map/layers/cluster/cluster-layer.tsx | 288 +- .../layers/cluster/donut-chart-cluster.tsx | 178 +- .../map/layers/mobile/color-palette.ts | 22 +- .../map/layers/mobile/mobile-box-layer.tsx | 388 +- .../map/layers/mobile/mobile-box-view.tsx | 248 +- .../layers/mobile/mobile-overview-layer.tsx | 176 +- app/components/map/legend.tsx | 2 +- app/components/map/map.tsx | 2 +- app/components/mydevices/dt/data-table.tsx | 44 +- .../edit-device/edit-device-sidebar-nav.tsx | 74 +- app/components/search/index.tsx | 222 +- app/components/search/search-list-item.tsx | 88 +- app/components/search/search-list.tsx | 1 - app/components/sensor-icon.tsx | 22 +- app/components/sidebar-settings-nav.tsx | 70 +- app/components/spinner/index.tsx | 84 +- app/components/ui/accordion.tsx | 80 +- app/components/ui/alert-dialog.tsx | 184 +- app/components/ui/alert.tsx | 10 +- app/components/ui/animated-counter.tsx | 82 +- app/components/ui/aspect-ratio.tsx | 4 +- app/components/ui/avatar.tsx | 62 +- app/components/ui/badge.tsx | 50 +- app/components/ui/breadcrumb.tsx | 173 +- app/components/ui/button.tsx | 90 +- app/components/ui/calendar.tsx | 121 +- app/components/ui/card.tsx | 104 +- app/components/ui/checkbox.tsx | 42 +- app/components/ui/command.tsx | 242 +- app/components/ui/dialog.tsx | 200 +- app/components/ui/drawer.tsx | 152 +- app/components/ui/dropdown-menu.tsx | 278 +- app/components/ui/form.tsx | 278 +- app/components/ui/hover-card.tsx | 34 +- app/components/ui/input.tsx | 34 +- app/components/ui/label.tsx | 28 +- app/components/ui/popover.tsx | 38 +- app/components/ui/radio-group.tsx | 60 +- app/components/ui/scroll-area.tsx | 70 +- app/components/ui/select.tsx | 170 +- app/components/ui/separator.tsx | 44 +- app/components/ui/sheet.tsx | 228 +- app/components/ui/switch.tsx | 40 +- app/components/ui/table.tsx | 156 +- app/components/ui/tabs.tsx | 68 +- app/components/ui/textarea.tsx | 32 +- app/components/ui/toast.tsx | 204 +- app/components/ui/toaster.tsx | 58 +- app/components/ui/toggle-group.tsx | 82 +- app/components/ui/toggle.tsx | 64 +- app/components/ui/tooltip.tsx | 30 +- app/components/ui/use-toast.ts | 284 +- app/cookies.ts | 20 +- app/entry.client.tsx | 86 +- app/entry.server.tsx | 152 +- app/i18next.server.ts | 50 +- app/lib/date-ranges.ts | 238 +- app/lib/decoding-service.server.ts | 72 +- app/lib/device-service.server.ts | 14 +- app/lib/devices-service.server.ts | 240 +- app/lib/directus.ts | 64 +- app/lib/helpers.ts | 53 +- app/lib/mobile-box-helper.ts | 193 +- app/lib/openapi.ts | 188 +- app/lib/outlier-transform.ts | 88 +- app/lib/request-parsing.ts | 100 +- app/lib/search-map-helper.ts | 92 +- app/lib/set-language.server.ts | 4 +- app/lib/transfer-service.server.ts | 327 +- app/lib/user-service.ts | 82 +- app/lib/utils.ts | 156 +- app/models/badge.server.ts | 153 +- app/models/device.server.ts | 18 +- app/models/log-entry.server.ts | 94 +- app/models/phenomena.server.ts | 20 +- app/models/transfer.server.ts | 203 +- app/models/unit.server.ts | 16 +- app/routes.ts | 6 +- app/routes/_index.tsx | 498 +- app/routes/account.settings.tsx | 914 +- app/routes/action.set-language.tsx | 24 +- app/routes/api.boxes.$deviceId.data.ts | 112 +- app/routes/api.boxes.$deviceId.locations.ts | 160 +- app/routes/api.claim.ts | 96 +- app/routes/api.devices.ts | 282 +- app/routes/api.getsensors.ts | 40 +- app/routes/api.measurements.ts | 43 +- app/routes/api.sign-out.ts | 47 +- app/routes/api.stats.ts | 42 +- app/routes/api.tags.ts | 26 +- app/routes/api.transfer.$deviceId.ts | 236 +- app/routes/api.transfer.ts | 219 +- app/routes/api.users.confirm-email.ts | 18 +- app/routes/api.users.me.boxes.$deviceId.ts | 56 +- app/routes/api.users.me.boxes.ts | 52 +- .../api.users.me.resend-email-confirmation.ts | 46 +- app/routes/api.users.me.ts | 202 +- app/routes/api.users.password-reset.ts | 98 +- app/routes/api.users.refresh-auth.ts | 95 +- app/routes/api.users.register.ts | 166 +- .../api.users.request-password-reset.ts | 57 +- app/routes/api.users.sign-in.ts | 81 +- app/routes/device.$deviceId.dataupload.tsx | 11 +- app/routes/device.$deviceId.edit.general.tsx | 237 +- app/routes/device.$deviceId.edit.location.tsx | 456 +- app/routes/device.$deviceId.edit.logs.tsx | 431 +- app/routes/device.$deviceId.edit.mqtt.tsx | 549 +- app/routes/device.$deviceId.edit.script.tsx | 152 +- app/routes/device.$deviceId.edit.sensors.tsx | 949 +- app/routes/device.$deviceId.edit.transfer.tsx | 206 +- app/routes/device.$deviceId.edit.tsx | 299 +- app/routes/device.$deviceId.edit.ttn.tsx | 340 +- app/routes/device.$deviceId.overview.tsx | 225 +- app/routes/device.dashboard.$deviceId.tsx | 468 +- app/routes/device.new.tsx | 133 +- app/routes/device.transfer.tsx | 63 +- app/routes/docs.tsx | 50 +- app/routes/explore.$deviceId.tsx | 24 +- app/routes/explore.forgot.tsx | 346 +- app/routes/explore.login.tsx | 380 +- app/routes/explore.register.tsx | 486 +- app/routes/healthcheck.tsx | 38 +- app/routes/impressum.tsx | 19 +- app/routes/join.tsx | 476 +- app/routes/login.tsx | 380 +- app/routes/logout.tsx | 16 +- ...ources.measurement.$deviceId.$sensorId.tsx | 30 +- app/routes/resources.user-avatar.tsx | 60 +- app/routes/settings.account.tsx | 24 +- app/routes/settings.notifications.tsx | 40 +- app/routes/settings.profile.photo.tsx | 382 +- app/routes/settings.profile.tsx | 381 +- app/routes/settings.tsx | 122 +- app/schema/claim.ts | 6 +- app/schema/log-entry.ts | 48 +- app/schema/password.ts | 72 +- app/schema/profile-image.ts | 44 +- app/schema/profile.ts | 62 +- app/schema/refreshToken.ts | 52 +- app/schema/sensor.ts | 108 +- app/schema/types.ts | 16 +- app/schema/user.ts | 90 +- app/styles/app.css | 322 +- app/styles/tailwind.css | 390 +- app/utils/device.ts | 28 +- app/utils/env.server.ts | 84 +- app/utils/file-exports.ts | 194 +- app/utils/file-upload.server.ts | 28 +- app/utils/forms.tsx | 140 +- app/utils/misc.ts | 38 +- app/utils/model-definitions.ts | 2 +- app/utils/param-utils.ts | 51 +- app/utils/s3.server.ts | 2 +- app/utils/sensor-definitions.ts | 698 +- app/utils/sensor-wiki-helper.tsx | 74 +- app/utils/sensoricons.tsx | 156 +- app/utils/session.server.ts | 220 +- app/utils/use-hydrated.ts | 16 +- app/utils/user-validation.ts | 34 +- app/utils/zod-extensions.ts | 18 +- components.json | 28 +- db/env-schema.ts | 18 +- db/seed.ts | 251 +- drizzle.config.ts | 20 +- mocks/README.md | 6 +- mocks/index.js | 12 +- other/README.md | 2 +- package-lock.json | 60094 ++++++++-------- postcss.config.js | 12 +- public/locales/de/connect.json | 14 +- public/locales/de/download.json | 62 +- public/locales/de/features.json | 20 +- public/locales/de/footer.json | 14 +- public/locales/de/header.json | 14 +- public/locales/de/integrations.json | 12 +- public/locales/de/landing.json | 6 +- public/locales/de/login.json | 47 +- public/locales/de/menu.json | 42 +- public/locales/de/navbar.json | 44 +- public/locales/de/newdevice.json | 110 +- public/locales/de/partners.json | 6 +- public/locales/de/pricing-plans.json | 12 +- public/locales/de/profile.json | 14 +- public/locales/de/register.json | 42 +- public/locales/de/search.json | 8 +- public/locales/de/stats.json | 6 +- public/locales/de/tools.json | 4 +- public/locales/en/connect.json | 14 +- public/locales/en/data-table.json | 44 +- public/locales/en/download.json | 62 +- public/locales/en/features.json | 20 +- public/locales/en/footer.json | 14 +- public/locales/en/header.json | 14 +- public/locales/en/integrations.json | 12 +- public/locales/en/landing.json | 6 +- public/locales/en/login.json | 48 +- public/locales/en/menu.json | 42 +- public/locales/en/navbar.json | 44 +- public/locales/en/newdevice.json | 112 +- public/locales/en/partners.json | 6 +- public/locales/en/pricing-plans.json | 12 +- public/locales/en/profile.json | 14 +- public/locales/en/register.json | 42 +- public/locales/en/search.json | 8 +- public/locales/en/stats.json | 6 +- public/locales/en/tools.json | 4 +- scripts/generate-openapi.ts | 12 +- tests/data/byte_submit_data.ts | 64 +- tests/data/csv_example_data.ts | 113 +- tests/data/index.ts | 6 +- tests/data/json_submit_data.ts | 37 +- tests/lib/device-transform.spec.ts | 471 +- tests/request-parsing.spec.ts | 381 +- tests/routes/api.users.confirm-email.spec.ts | 64 +- tests/routes/api.users.password-reset.spec.ts | 98 +- tests/routes/api.users.sign-in.spec.ts | 314 +- tests/utils.spec.ts | 22 +- types/index.d.ts | 2 +- vite.config.ts | 84 +- vitest.setup.ts | 12 +- 274 files changed, 47828 insertions(+), 47438 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a8fc9a4a..7cbd6833 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -12,4 +12,6 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username -custom: ['https://www.betterplace.org/en/projects/89947-opensensemap-org-the-free-map-for-environmental-data'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: [ + 'https://www.betterplace.org/en/projects/89947-opensensemap-org-the-free-map-for-environmental-data', + ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 8150d9fd..0e7ad64c 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,15 +1,15 @@ version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "weekly" - - package-ecosystem: "docker" - directory: "/" + interval: 'weekly' + - package-ecosystem: 'docker' + directory: '/' schedule: - interval: "weekly" - - package-ecosystem: "npm" - directory: "/" + interval: 'weekly' + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" \ No newline at end of file + interval: 'weekly' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dbc190e6..4115c082 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,7 +81,7 @@ jobs: # the sleep is just there to give time for postgres to get started run: docker compose -f docker-compose.ci.yml up -d && sleep 30 env: - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres" + DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/postgres' - name: 🛠 Setup Database uses: nick-fields/retry@v3.0.2 diff --git a/.github/workflows/purge-pr.yml b/.github/workflows/purge-pr.yml index 19b9db76..d5ec743b 100644 --- a/.github/workflows/purge-pr.yml +++ b/.github/workflows/purge-pr.yml @@ -17,4 +17,4 @@ jobs: organization: ${{ github.repository_owner}} container: ${{ github.event.repository.name }} tag-regex: pr-${{github.event.pull_request.number}}$ - dry-run: false \ No newline at end of file + dry-run: false diff --git a/.github/workflows/purge.yml b/.github/workflows/purge.yml index d6c65043..468eed98 100644 --- a/.github/workflows/purge.yml +++ b/.github/workflows/purge.yml @@ -1,7 +1,7 @@ name: 🗑️ Purge untagged images on: schedule: - - cron: "0 0 * * *" + - cron: '0 0 * * *' permissions: packages: write @@ -16,4 +16,4 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} organization: ${{ github.repository_owner}} container: ${{ github.event.repository.name }} - prune-untagged: true \ No newline at end of file + prune-untagged: true diff --git a/README.md b/README.md index e54ffe4f..6c185160 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ ![openSenseMap](https://github.com/openSenseMap/frontend/blob/dev/public/openSenseMap.png) -This repository contains the code of the new _openSenseMap_ frontend running at [https://beta.opensensemap.org](https://beta.opensensemap.org). +This repository contains the code of the new _openSenseMap_ frontend running at +[https://beta.opensensemap.org](https://beta.opensensemap.org). -Originally, the _openSenseMap_ was built as part of the bachelor thesis of [@mpfeil](https://github.com/mpfeil) at the ifgi (Institute for Geoinformatics, University of Münster). Between 2016 and 2022 development was partly funded by the German Ministry of Education and Research (BMBF) in the projets senseBox and senseBox Pro. This version has been developed by [@mpfeil](https://github.com/mpfeil) and [@freds-dev](https://github.com/freds-dev). +Originally, the _openSenseMap_ was built as part of the bachelor thesis of +[@mpfeil](https://github.com/mpfeil) at the ifgi (Institute for Geoinformatics, +University of Münster). Between 2016 and 2022 development was partly funded by +the German Ministry of Education and Research (BMBF) in the projets senseBox and +senseBox Pro. This version has been developed by +[@mpfeil](https://github.com/mpfeil) and +[@freds-dev](https://github.com/freds-dev). Screenshot OSeM ## Project setup -If you do need to set the project up locally yourself, feel free to follow these instructions: +If you do need to set the project up locally yourself, feel free to follow these +instructions: ### System Requirements @@ -19,7 +27,8 @@ If you do need to set the project up locally yourself, feel free to follow these ### Variables -You can configure the API endpoint and/or map tiles using the following environmental variables: +You can configure the API endpoint and/or map tiles using the following +environmental variables: | ENV | Default value | | ------------------- | ------------------------------------ | @@ -34,15 +43,19 @@ You can create a copy of `.env.example`, rename it to `.env` and set the values. 1. Clone the repo: `git clone https://github.com/openSenseMap/frontend` 2. Copy `.env.example` into `.env` 3. Run `npm install` to install dependencies -4. Optionally run `docker compose up` to start a docker container running your local postgres DB - - If it is the first time doing this, you may need to bootstrap the database by running `npm run db:setup` - - If you want some example data run `npm run db:seed`. **WARNING**: Do not run this on a production database. It will delete all existing data. +4. Optionally run `docker compose up` to start a docker container running your + local postgres DB + - If it is the first time doing this, you may need to bootstrap the database + by running `npm run db:setup` + - If you want some example data run `npm run db:seed`. **WARNING**: Do not + run this on a production database. It will delete all existing data. 5. Run `npm run dev` to start the local server ### Contributing -We welcome all kind of constructive contributions to this project. -If you are planning to implement a new feature or change something, please create an issue first. +We welcome all kind of constructive contributions to this project. If you are +planning to implement a new feature or change something, please create an issue +first. Afterwards follow these steps: @@ -50,7 +63,8 @@ Afterwards follow these steps: 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Make and commit your changes 4. Push to the branch (`git push origin my-new-feature`) -5. Create a new pull request against this repository's `dev` branch, linking your issue. +5. Create a new pull request against this repository's `dev` branch, linking + your issue. #### How the repository is organized @@ -75,18 +89,39 @@ Afterwards follow these steps: #### openSenseMap API -The api is implemented using [Remix resource routes](https://remix.run/docs/en/main/guides/resource-routes). -Resource routes may not export a component but only [loaders](https://remix.run/docs/en/main/route/loader) (for `GET` requests) and [actions](https://remix.run/docs/en/main/route/action) (for `POST`, `PUT`, `DELETE` etc) and therefore live in `.ts` (not `.tsx`) files. -All resource routes start with `api` (e.g. `api.user.ts` for `/api/user`). - -The api logic is shared with the frontend. Therefore api routes should not implement the actual business logic of an endpoint. They are responsible for checking the request for validity and for transforming the data into the correct output format. -Logic should be implemented in corresponding services, that may be used by loaders/ actions of page routes that access the same functionality. - -For example: User registration is possible from both the api and the frontend. The logic for it is implemented in `lib/user.service.ts` and it is being used by both `api.user.ts` (resource route) as well as `explore.register.tsx` (page route), preventing duplication of common logic while also providing the flexibility to adjust the outputs to the needs of the respective use case. +The api is implemented using +[Remix resource routes](https://remix.run/docs/en/main/guides/resource-routes). +Resource routes may not export a component but only +[loaders](https://remix.run/docs/en/main/route/loader) (for `GET` requests) and +[actions](https://remix.run/docs/en/main/route/action) (for `POST`, `PUT`, +`DELETE` etc) and therefore live in `.ts` (not `.tsx`) files. All resource +routes start with `api` (e.g. `api.user.ts` for `/api/user`). + +The api logic is shared with the frontend. Therefore api routes should not +implement the actual business logic of an endpoint. They are responsible for +checking the request for validity and for transforming the data into the correct +output format. Logic should be implemented in corresponding services, that may +be used by loaders/ actions of page routes that access the same functionality. + +For example: User registration is possible from both the api and the frontend. +The logic for it is implemented in `lib/user.service.ts` and it is being used by +both `api.user.ts` (resource route) as well as `explore.register.tsx` (page +route), preventing duplication of common logic while also providing the +flexibility to adjust the outputs to the needs of the respective use case. ##### Documenting an API Route -The [swaggerJsdoc Library](https://www.npmjs.com/package/swagger-jsdoc) reads the JSDoc-annotated source code in the api-routes and generates an openAPI(Swagger) specification and is rendered using [Swaggger UI](https://swagger.io/tools/swagger-ui/). The [JSDoc annotations](https://github.com/Surnet/swagger-jsdoc) is usually added before the loader or action function in the API Routes. The documentation will then be automatically generated from the JSDoc annotations in all the api routes. When testing the api during development do not forget to change the server to [Development Server](http://localhost:3000). To authorize a user you must provide the token obtained after sign-in. You can just copy and paste the token in the value field and then hit the authorize button. +The [swaggerJsdoc Library](https://www.npmjs.com/package/swagger-jsdoc) reads +the JSDoc-annotated source code in the api-routes and generates an +openAPI(Swagger) specification and is rendered using +[Swaggger UI](https://swagger.io/tools/swagger-ui/). The +[JSDoc annotations](https://github.com/Surnet/swagger-jsdoc) is usually added +before the loader or action function in the API Routes. The documentation will +then be automatically generated from the JSDoc annotations in all the api +routes. When testing the api during development do not forget to change the +server to [Development Server](http://localhost:3000). To authorize a user you +must provide the token obtained after sign-in. You can just copy and paste the +token in the value field and then hit the authorize button. ##### JSDoc Example @@ -144,29 +179,35 @@ Here's an example of how to document an API route using JSDoc annotations: * description: Internal server error */ export async function loader({ params }) { - const { id } = params; - - try { - const user = await getUserById(id); - if (!user) { - throw new Response("User not found", { status: 404 }); - } - return Response.json({ user }); - } catch (error) { - throw new Response("Internal server error", { status: 500 }); - } + const { id } = params + + try { + const user = await getUserById(id) + if (!user) { + throw new Response('User not found', { status: 404 }) + } + return Response.json({ user }) + } catch (error) { + throw new Response('Internal server error', { status: 500 }) + } } ``` -This JSDoc annotation will automatically generate comprehensive API documentation including endpoint details, parameters, response schemas, and example values. +This JSDoc annotation will automatically generate comprehensive API +documentation including endpoint details, parameters, response schemas, and +example values. #### Testing -Tests are placed in the [tests/](./tests/) folder whose structure is similar to the [app/](./app/) folder. -When adding a test, use the same name as the file you are testing but change the file extension to `.spec.ts`, e.g. when creating tests for [`./app/utils`](./app/utils.ts) name the test file [`./tests/utils.spec.ts`](./tests/utils.spec.ts). +Tests are placed in the [tests/](./tests/) folder whose structure is similar to +the [app/](./app/) folder. When adding a test, use the same name as the file you +are testing but change the file extension to `.spec.ts`, e.g. when creating +tests for [`./app/utils`](./app/utils.ts) name the test file +[`./tests/utils.spec.ts`](./tests/utils.spec.ts). -To run the tests, make sure you have a working database connection (e.g. by running `docker compose up` with the corresponding environment variables to use your local database). -Then simply run `npm test`. +To run the tests, make sure you have a working database connection (e.g. by +running `docker compose up` with the corresponding environment variables to use +your local database). Then simply run `npm test`. ## License diff --git a/app/components/aggregation-filter.tsx b/app/components/aggregation-filter.tsx index 71147999..18326765 100644 --- a/app/components/aggregation-filter.tsx +++ b/app/components/aggregation-filter.tsx @@ -1,83 +1,83 @@ -import * as SelectPrimitive from "@radix-ui/react-select"; -import { Filter } from "lucide-react"; -import { useSearchParams, useSubmit } from "react-router"; -import { Badge } from "./ui/badge"; +import * as SelectPrimitive from '@radix-ui/react-select' +import { Filter } from 'lucide-react' +import { useSearchParams, useSubmit } from 'react-router' +import { Badge } from './ui/badge' -import { Select, SelectContent, SelectItem } from "./ui/select"; -import { Separator } from "./ui/separator"; +import { Select, SelectContent, SelectItem } from './ui/select' +import { Separator } from './ui/separator' type Aggregation = { - value: string; - label: string; -}; + value: string + label: string +} const aggregations: Aggregation[] = [ - { - value: "raw", - label: "Raw", - }, - { - value: "10m", - label: "10 Minutes", - }, - { - value: "1h", - label: "1 Hour", - }, - { - value: "1d", - label: "1 Day", - }, - { - value: "1m", - label: "1 Month", - }, - { - value: "1y", - label: "1 Year", - }, -]; + { + value: 'raw', + label: 'Raw', + }, + { + value: '10m', + label: '10 Minutes', + }, + { + value: '1h', + label: '1 Hour', + }, + { + value: '1d', + label: '1 Day', + }, + { + value: '1m', + label: '1 Month', + }, + { + value: '1y', + label: '1 Year', + }, +] export function AggregationFilter() { - const submit = useSubmit(); - const [searchParams] = useSearchParams(); + const submit = useSubmit() + const [searchParams] = useSearchParams() - const aggregationParam = searchParams.get("aggregation") || "raw"; - const selectedAggregation = aggregations.find( - (aggregation) => aggregation.value === aggregationParam, - ); + const aggregationParam = searchParams.get('aggregation') || 'raw' + const selectedAggregation = aggregations.find( + (aggregation) => aggregation.value === aggregationParam, + ) - return ( - - ); + return ( + + ) } diff --git a/app/components/client-only.tsx b/app/components/client-only.tsx index 441096bc..4653e6ce 100644 --- a/app/components/client-only.tsx +++ b/app/components/client-only.tsx @@ -1,15 +1,15 @@ -import * as React from "react"; -import { useHydrated } from "~/utils/use-hydrated"; +import * as React from 'react' +import { useHydrated } from '~/utils/use-hydrated' type Props = { - /** - * You are encouraged to add a fallback that is the same dimensions - * as the client rendered children. This will avoid content layout - * shift which is disgusting - */ - children(): React.ReactNode; - fallback?: React.ReactNode; -}; + /** + * You are encouraged to add a fallback that is the same dimensions + * as the client rendered children. This will avoid content layout + * shift which is disgusting + */ + children(): React.ReactNode + fallback?: React.ReactNode +} /** * Render the children only after the JS has loaded client-side. Use an optional @@ -27,5 +27,5 @@ type Props = { * ``` */ export function ClientOnly({ children, fallback = null }: Props) { - return useHydrated() ? <>{children()} : <>{fallback}; + return useHydrated() ? <>{children()} : <>{fallback} } diff --git a/app/components/color-picker.tsx b/app/components/color-picker.tsx index 10493c26..7b62dfa3 100644 --- a/app/components/color-picker.tsx +++ b/app/components/color-picker.tsx @@ -1,79 +1,79 @@ -"use client"; +'use client' -import { X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' export function ColorPicker({ - handleColorChange, - colorPickerState, - setColorPickerState, + handleColorChange, + colorPickerState, + setColorPickerState, }: { - handleColorChange: (color: string, index: number) => void; - colorPickerState: { - open: boolean; - index: number; - color: string; - }; - setColorPickerState: (state: { - open: boolean; - index: number; - color: string; - }) => void; - className?: string; + handleColorChange: (color: string, index: number) => void + colorPickerState: { + open: boolean + index: number + color: string + } + setColorPickerState: (state: { + open: boolean + index: number + color: string + }) => void + className?: string }) { - const solids = [ - "#E2E2E2", - "#ff75c3", - "#ffa647", - "#ffe83f", - "#9fff5b", - "#70e2ff", - "#cd93ff", - "#09203f", - ]; + const solids = [ + '#E2E2E2', + '#ff75c3', + '#ffa647', + '#ffe83f', + '#9fff5b', + '#70e2ff', + '#cd93ff', + '#09203f', + ] - function onClose() { - setColorPickerState({ ...colorPickerState, open: false }); - } + function onClose() { + setColorPickerState({ ...colorPickerState, open: false }) + } - return ( -
-
-

Choose or set a color

- -
-
- {solids.map((color) => ( -
-
-
- { - handleColorChange(e.target.value, colorPickerState.index); - setColorPickerState({ ...colorPickerState, color: e.target.value }); - }} - /> -
-
- ); + return ( +
+
+

Choose or set a color

+ +
+
+ {solids.map((color) => ( +
+
+
+ { + handleColorChange(e.target.value, colorPickerState.index) + setColorPickerState({ ...colorPickerState, color: e.target.value }) + }} + /> +
+
+ ) } diff --git a/app/components/daterange-filter.tsx b/app/components/daterange-filter.tsx index ba8d12f6..40f4f159 100644 --- a/app/components/daterange-filter.tsx +++ b/app/components/daterange-filter.tsx @@ -1,171 +1,171 @@ -import { PopoverClose } from "@radix-ui/react-popover"; -import { format } from "date-fns"; -import { Clock } from "lucide-react"; -import { useEffect, useState } from "react"; -import { type DateRange } from "react-day-picker"; -import { useLoaderData, useSearchParams, useSubmit } from "react-router"; -import { Badge } from "./ui/badge"; -import { Button } from "./ui/button"; -import { Calendar } from "./ui/calendar"; +import { PopoverClose } from '@radix-ui/react-popover' +import { format } from 'date-fns' +import { Clock } from 'lucide-react' +import { useEffect, useState } from 'react' +import { type DateRange } from 'react-day-picker' +import { useLoaderData, useSearchParams, useSubmit } from 'react-router' +import { Badge } from './ui/badge' +import { Button } from './ui/button' +import { Calendar } from './ui/calendar' import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "./ui/command"; -import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { Separator } from "./ui/separator"; -import dateTimeRanges from "~/lib/date-ranges"; -import { type loader } from "~/routes/explore.$deviceId.$sensorId.$"; + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from './ui/command' +import { Popover, PopoverContent, PopoverTrigger } from './ui/popover' +import { Separator } from './ui/separator' +import dateTimeRanges from '~/lib/date-ranges' +import { type loader } from '~/routes/explore.$deviceId.$sensorId.$' export function DateRangeFilter() { - // Get data from the loader - const loaderData = useLoaderData(); + // Get data from the loader + const loaderData = useLoaderData() - // Form submission handler - const submit = useSubmit(); - const [searchParams] = useSearchParams(); + // Form submission handler + const submit = useSubmit() + const [searchParams] = useSearchParams() - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false) - // State for selected date range and aggregation - const [date, setDate] = useState({ - from: loaderData.startDate ? new Date(loaderData.startDate) : undefined, - to: loaderData.endDate ? new Date(loaderData.endDate) : undefined, - }); + // State for selected date range and aggregation + const [date, setDate] = useState({ + from: loaderData.startDate ? new Date(loaderData.startDate) : undefined, + to: loaderData.endDate ? new Date(loaderData.endDate) : undefined, + }) - if ( - !date?.from && - !date?.to && - loaderData.sensors && - loaderData.sensors.length > 0 && - loaderData.sensors[0].data && - loaderData.sensors[0].data.length > 0 - ) { - const firstDate = loaderData.sensors[0].data[0]?.time; - const lastDate = - loaderData.sensors[0].data[loaderData.sensors[0].data.length - 1]?.time; + if ( + !date?.from && + !date?.to && + loaderData.sensors && + loaderData.sensors.length > 0 && + loaderData.sensors[0].data && + loaderData.sensors[0].data.length > 0 + ) { + const firstDate = loaderData.sensors[0].data[0]?.time + const lastDate = + loaderData.sensors[0].data[loaderData.sensors[0].data.length - 1]?.time - setDate({ - from: lastDate ? new Date(lastDate) : undefined, - to: firstDate ? new Date(firstDate) : undefined, - }); - } + setDate({ + from: lastDate ? new Date(lastDate) : undefined, + to: firstDate ? new Date(firstDate) : undefined, + }) + } - // Shortcut to open date range selection - useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "d" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen((open) => !open); - } - }; + // Shortcut to open date range selection + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'd' && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + setOpen((open) => !open) + } + } - document.addEventListener("keydown", down); + document.addEventListener('keydown', down) - return () => { - document.removeEventListener("keydown", down); - }; - }, []); + return () => { + document.removeEventListener('keydown', down) + } + }, []) - // Update search params when date or aggregation changes - useEffect(() => { - if (date?.from) { - searchParams.set("date_from", date?.from?.toISOString() ?? ""); - } - if (date?.to) { - searchParams.set("date_to", date?.to?.toISOString() ?? ""); - } - }, [date, searchParams]); + // Update search params when date or aggregation changes + useEffect(() => { + if (date?.from) { + searchParams.set('date_from', date?.from?.toISOString() ?? '') + } + if (date?.to) { + searchParams.set('date_to', date?.to?.toISOString() ?? '') + } + }, [date, searchParams]) - return ( - - - - - -
-
-
-
- Absolute time range -
- { - setDate(dates); - }} - initialFocus - /> -
- - - - - No range found. - - {dateTimeRanges.map((dateTimeRange) => ( - { - const selectedDateTimeRange = dateTimeRanges.find( - (range) => range.value === value, - ); + return ( + + + + + +
+
+
+
+ Absolute time range +
+ { + setDate(dates) + }} + initialFocus + /> +
+ + + + + No range found. + + {dateTimeRanges.map((dateTimeRange) => ( + { + const selectedDateTimeRange = dateTimeRanges.find( + (range) => range.value === value, + ) - const timeRange = selectedDateTimeRange?.convert(); + const timeRange = selectedDateTimeRange?.convert() - setDate({ - from: timeRange?.from, - to: timeRange?.to, - }); - }} - > - {dateTimeRange.label} - - ))} - - - -
-
- { - void submit(searchParams); - }} - > - Apply - -
-
-
-
- ); + setDate({ + from: timeRange?.from, + to: timeRange?.to, + }) + }} + > + {dateTimeRange.label} +
+ ))} +
+
+
+
+
+ { + void submit(searchParams) + }} + > + Apply + +
+
+
+
+ ) } diff --git a/app/components/device-card.tsx b/app/components/device-card.tsx index af8a81b4..733c3a62 100644 --- a/app/components/device-card.tsx +++ b/app/components/device-card.tsx @@ -1,35 +1,35 @@ -import { Circle } from "lucide-react"; +import { Circle } from 'lucide-react' import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "./ui/card"; -import { type Device } from "~/schema"; + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './ui/card' +import { type Device } from '~/schema' interface DeviceCardProps { - device: Device; + device: Device } export default function DeviceCard({ device }: DeviceCardProps) { - return ( - - -
- {device.name} - {device.description} -
-
- -
-
- - {device.model} -
-
Updated {device.updatedAt.toString()}
-
-
-
- ); + return ( + + +
+ {device.name} + {device.description} +
+
+ +
+
+ + {device.model} +
+
Updated {device.updatedAt.toString()}
+
+
+
+ ) } diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index 1b09b45e..c11855af 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -512,7 +512,8 @@ export default function DeviceDetailBox() {
- {sensor.lastMeasurement?.value ?? '–'} + {sensor.lastMeasurement?.value ?? + '–'}

{sensor.unit} @@ -588,7 +589,8 @@ export default function DeviceDetailBox() {

- {sensor.lastMeasurement?.value ?? '–'} + {sensor.lastMeasurement?.value ?? + '–'}

{sensor.unit} diff --git a/app/components/device-detail/entry-logs.tsx b/app/components/device-detail/entry-logs.tsx index 5eabf15f..098923d7 100644 --- a/app/components/device-detail/entry-logs.tsx +++ b/app/components/device-detail/entry-logs.tsx @@ -1,165 +1,167 @@ -import { useMediaQuery } from "@mantine/hooks"; -import { Activity, Clock, ExternalLink } from "lucide-react"; -import { useState } from "react"; -import { Button } from "../ui/button"; +import { useMediaQuery } from '@mantine/hooks' +import { Activity, Clock, ExternalLink } from 'lucide-react' +import { useState } from 'react' +import { Button } from '../ui/button' import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog' import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "../ui/drawer"; + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '../ui/drawer' import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; -import { Card } from "@/components/ui/card"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { type LogEntry } from "~/schema/log-entry"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../ui/tooltip' +import { Card } from '@/components/ui/card' +import { ScrollArea } from '@/components/ui/scroll-area' +import { type LogEntry } from '~/schema/log-entry' export default function EntryLogs({ - entryLogs = [], + entryLogs = [], }: { - entryLogs: LogEntry[]; + entryLogs: LogEntry[] }) { - const [open, setOpen] = useState(false); - const isDesktop = useMediaQuery("(min-width: 768px)"); + const [open, setOpen] = useState(false) + const isDesktop = useMediaQuery('(min-width: 768px)') - if (isDesktop) { - return ( -

-

Logs

-
-
-
- -
-
-

{entryLogs[entryLogs.length -1].content}

-
- - {new Date(entryLogs[0].createdAt).toLocaleString()} -
-
-
-
- - - - - - - Device Logs - - If this is your device, you can make changes in your device - settings. - - - - - -
-
-
- ); - } + if (isDesktop) { + return ( +
+

Logs

+
+
+
+ +
+
+

+ {entryLogs[entryLogs.length - 1].content} +

+
+ + {new Date(entryLogs[0].createdAt).toLocaleString()} +
+
+
+
+ + + + + + + Device Logs + + If this is your device, you can make changes in your device + settings. + + + + + +
+
+
+ ) + } - return ( -
-

Logs

-
-
-
- -
-
-

{entryLogs[0].content}

-
- - {new Date(entryLogs[0].createdAt).toLocaleString()} -
-
-
-
- - - - - - - Device Logs - - If this is your device, you can make changes in your device - settings. - - - - - - - - - - -
-
- ); + return ( +
+

Logs

+
+
+
+ +
+
+

{entryLogs[0].content}

+
+ + {new Date(entryLogs[0].createdAt).toLocaleString()} +
+
+
+
+ + + + + + + Device Logs + + If this is your device, you can make changes in your device + settings. + + + + + + + + + + +
+
+ ) } function LogList({ entryLogs = [] }: { entryLogs: LogEntry[] }) { - return ( - -
- {entryLogs.map((log, index) => ( -
-
- -
-
- -

{log.content}

-
- - {new Date(log.createdAt).toLocaleString()} -
-
-
- {index < entryLogs.length - 1 && ( - - ))} -
- - ); + return ( + +
+ {entryLogs.map((log, index) => ( +
+
+ +
+
+ +

{log.content}

+
+ + {new Date(log.createdAt).toLocaleString()} +
+
+
+ {index < entryLogs.length - 1 && ( + + ))} +
+ + ) } diff --git a/app/components/device-detail/graph.tsx b/app/components/device-detail/graph.tsx index 594d983c..114675ba 100644 --- a/app/components/device-detail/graph.tsx +++ b/app/components/device-detail/graph.tsx @@ -13,7 +13,14 @@ import { import 'chartjs-adapter-date-fns' // import { de, enGB } from "date-fns/locale"; import { Download, RefreshCcw, X } from 'lucide-react' -import { useMemo, useRef, useState, useEffect, useContext,type RefObject } from 'react' +import { + useMemo, + useRef, + useState, + useEffect, + useContext, + type RefObject, +} from 'react' import { Scatter } from 'react-chartjs-2' import { isBrowser, isTablet } from 'react-device-detect' import Draggable, { type DraggableData } from 'react-draggable' diff --git a/app/components/device-detail/profile-box-selection.tsx b/app/components/device-detail/profile-box-selection.tsx index 10aadbc3..80c3597e 100644 --- a/app/components/device-detail/profile-box-selection.tsx +++ b/app/components/device-detail/profile-box-selection.tsx @@ -1,70 +1,70 @@ // import { useState } from "react"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../ui/card"; + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '../ui/card' import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select' const dummyBoxes = [ - { - name: "Box at IFGI", - id: "1", - image: "/sensebox_outdoor.jpg", - }, - { - name: "senseBox at Aasee", - id: "2", - image: "https://picsum.photos/200/300", - }, - { - name: "Box at Schlossgarten", - id: "3", - image: "https://picsum.photos/200/300", - }, -]; + { + name: 'Box at IFGI', + id: '1', + image: '/sensebox_outdoor.jpg', + }, + { + name: 'senseBox at Aasee', + id: '2', + image: 'https://picsum.photos/200/300', + }, + { + name: 'Box at Schlossgarten', + id: '3', + image: 'https://picsum.photos/200/300', + }, +] export default function ProfileBoxSelection() { - // const [selectedBox, setSelectedBox] = useState(dummyBoxes[0]); - return ( -
- {/* this is all jsut dummy data - the real data will be fetched from the API as soon as the route is implemented */} - - - {dummyBoxes[0].name} - Last activity: 13min ago - - -
- -
-
- - - -
-
- ); + // const [selectedBox, setSelectedBox] = useState(dummyBoxes[0]); + return ( +
+ {/* this is all jsut dummy data - the real data will be fetched from the API as soon as the route is implemented */} + + + {dummyBoxes[0].name} + Last activity: 13min ago + + +
+ +
+
+ + + +
+
+ ) } diff --git a/app/components/device-detail/share-link.tsx b/app/components/device-detail/share-link.tsx index 42066ac5..bd4d5077 100644 --- a/app/components/device-detail/share-link.tsx +++ b/app/components/device-detail/share-link.tsx @@ -1,99 +1,99 @@ -import { Copy, Link } from "lucide-react"; -import { Button } from "../ui/button"; -import { Input } from "../ui/input"; -import { useToast } from "@/components/ui/use-toast"; +import { Copy, Link } from 'lucide-react' +import { Button } from '../ui/button' +import { Input } from '../ui/input' +import { useToast } from '@/components/ui/use-toast' export default function ShareLink() { - const { toast } = useToast(); + const { toast } = useToast() - return ( -
-
- {/* */} -
- - - -
- {/* */} -
- - - -
- {/* */} -
- - - - - -
- {/* */} -
- - - -
- {/* */} -
- - - -
-
- {/* */} -
- - - -
-
- ); + return ( +
+
+ {/* */} +
+ + + +
+ {/* */} +
+ + + +
+ {/* */} +
+ + + + + +
+ {/* */} +
+ + + +
+ {/* */} +
+ + + +
+
+ {/* */} +
+ + + +
+
+ ) } diff --git a/app/components/device/new/advanced-info.tsx b/app/components/device/new/advanced-info.tsx index ba7019d2..240c8f20 100644 --- a/app/components/device/new/advanced-info.tsx +++ b/app/components/device/new/advanced-info.tsx @@ -1,246 +1,246 @@ -import { useFormContext } from "react-hook-form"; +import { useFormContext } from 'react-hook-form' import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "~/components/ui/card"; -import { Input } from "~/components/ui/input"; -import { Label } from "~/components/ui/label"; + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '~/components/ui/card' +import { Input } from '~/components/ui/input' +import { Label } from '~/components/ui/label' import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "~/components/ui/select"; -import { Switch } from "~/components/ui/switch"; -import { Textarea } from "~/components/ui/textarea"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '~/components/ui/select' +import { Switch } from '~/components/ui/switch' +import { Textarea } from '~/components/ui/textarea' export function AdvancedStep() { - const { register, setValue, watch, resetField } = useFormContext(); - - // Watch field states - const isMqttEnabled = watch("mqttEnabled") || false; - const isTtnEnabled = watch("ttnEnabled") || false; - - // Clear corresponding fields when disabling - const handleMqttToggle = (checked: boolean) => { - setValue("mqttEnabled", checked); - if (!checked) { - resetField("url"); - resetField("topic"); - resetField("messageFormat"); - resetField("decodeOptions"); - resetField("connectionOptions"); - } - }; - - const handleTtnToggle = (checked: boolean) => { - setValue("ttnEnabled", checked); - if (!checked) { - resetField("dev_id"); - resetField("app_id"); - resetField("profile"); - resetField("decodeOptions"); - resetField("port"); - } - }; - - const handleInputChange = ( - event: React.ChangeEvent, - ) => { - const { name, value } = event.target; - setValue(name, value); - }; - - const handleSelectChange = (field: string, value: string) => { - setValue(field, value); - }; - - return ( - <> - {/* MQTT Configuration */} - - - MQTT Configuration - - Configure your MQTT settings for data streaming - - - -
- - -
- - {isMqttEnabled && ( -
-
- - -
- -
- - -
- -
- - -
- -
- - + -
-
- Help section -
-
-
-
- ); +
  • +
    +

    + If you would like to upload the Sketch with the Arduino + IDE you can find more information + +   here. + +

    +
    +
  • + +
    + +
    +
    +
    + ) } export function ErrorBoundary() { - return ( -
    - -
    - ); + return ( +
    + +
    + ) } diff --git a/app/routes/device.$deviceId.edit.sensors.tsx b/app/routes/device.$deviceId.edit.sensors.tsx index b7f84414..0027b64d 100644 --- a/app/routes/device.$deviceId.edit.sensors.tsx +++ b/app/routes/device.$deviceId.edit.sensors.tsx @@ -1,517 +1,514 @@ import { - ChevronDownIcon, - Trash2, - ClipboardCopy, - Edit, - Plus, - Save, - Undo2, - X, -} from "lucide-react"; -import React, { useState } from "react"; -import { redirect , Form, useActionData, useLoaderData, useOutletContext, type ActionFunctionArgs, type LoaderFunctionArgs } from "react-router"; -import invariant from "tiny-invariant"; + ChevronDownIcon, + Trash2, + ClipboardCopy, + Edit, + Plus, + Save, + Undo2, + X, +} from 'lucide-react' +import React, { useState } from 'react' import { - DropdownMenu, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import ErrorMessage from "~/components/error-message"; + redirect, + Form, + useActionData, + useLoaderData, + useOutletContext, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from 'react-router' +import invariant from 'tiny-invariant' import { - addNewSensor, - deleteSensor, - getSensorsFromDevice, - updateSensor, -} from "~/models/sensor.server"; -import { assignIcon, getIcon, iconsList } from "~/utils/sensoricons"; -import { getUserId } from "~/utils/session.server"; + DropdownMenu, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import ErrorMessage from '~/components/error-message' +import { + addNewSensor, + deleteSensor, + getSensorsFromDevice, + updateSensor, +} from '~/models/sensor.server' +import { assignIcon, getIcon, iconsList } from '~/utils/sensoricons' +import { getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request, params }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') - const deviceID = params.deviceId; - if (typeof deviceID !== "string") { - return "deviceID not found"; - } - const rawSensorsData = await getSensorsFromDevice(deviceID); + const deviceID = params.deviceId + if (typeof deviceID !== 'string') { + return 'deviceID not found' + } + const rawSensorsData = await getSensorsFromDevice(deviceID) - return rawSensorsData as any; + return rawSensorsData as any } //***************************************************** export async function action({ request, params }: ActionFunctionArgs) { - //* ToDo: upadte it to include button clicks inside form - const formData = await request.formData(); - const { updatedSensorsData } = Object.fromEntries(formData); + //* ToDo: upadte it to include button clicks inside form + const formData = await request.formData() + const { updatedSensorsData } = Object.fromEntries(formData) - if (typeof updatedSensorsData !== "string") { - return { isUpdated: false }; - } - const updatedSensorsDataJson = JSON.parse(updatedSensorsData); + if (typeof updatedSensorsData !== 'string') { + return { isUpdated: false } + } + const updatedSensorsDataJson = JSON.parse(updatedSensorsData) - for (const sensor of updatedSensorsDataJson) { - if (sensor?.new === true && sensor?.edited === true) { - const deviceID = params.deviceId; - invariant(deviceID, `deviceID not found!`); + for (const sensor of updatedSensorsDataJson) { + if (sensor?.new === true && sensor?.edited === true) { + const deviceID = params.deviceId + invariant(deviceID, `deviceID not found!`) - await addNewSensor({ - title: sensor.title, - unit: sensor.unit, - sensorType: sensor.sensorType, - deviceId: deviceID, - }); - } else if (sensor?.edited === true) { - await updateSensor({ - id: sensor.id, - title: sensor.title, - unit: sensor.unit, - sensorType: sensor.sensorType, - // icon: sensor.icon, - }); - } else if (sensor?.deleted === true) { - await deleteSensor(sensor.id); - } - } + await addNewSensor({ + title: sensor.title, + unit: sensor.unit, + sensorType: sensor.sensorType, + deviceId: deviceID, + }) + } else if (sensor?.edited === true) { + await updateSensor({ + id: sensor.id, + title: sensor.title, + unit: sensor.unit, + sensorType: sensor.sensorType, + // icon: sensor.icon, + }) + } else if (sensor?.deleted === true) { + await deleteSensor(sensor.id) + } + } - return { isUpdated: true }; + return { isUpdated: true } } //********************************** export default function EditBoxSensors() { - const data = useLoaderData(); - const actionData = useActionData(); + const data = useLoaderData() + const actionData = useActionData() - const [sensorsData, setSensorsData] = useState(data); + const [sensorsData, setSensorsData] = useState(data) - /* temp impl. until figuring out how to updating state of nested objects */ - const [tepmState, setTepmState] = useState(false); - //* to view toast on edit-page - const [setToastOpen] = useOutletContext<[(_open: boolean) => void]>(); + /* temp impl. until figuring out how to updating state of nested objects */ + const [tepmState, setTepmState] = useState(false) + //* to view toast on edit-page + const [setToastOpen] = useOutletContext<[(_open: boolean) => void]>() - React.useEffect(() => { - //* if sensors data were updated successfully - if (actionData && actionData?.isUpdated) { - //* show notification when data is successfully updated - setToastOpen(true); - // window.location.reload(); - //* reset sensor data elements - for (let index = 0; index < sensorsData.length; index++) { - const sensor = sensorsData[index]; - if (sensor.new == true && sensor.notValidInput == true) { - sensorsData.splice(index, 1); - } else if (sensor.deleted) { - sensorsData.splice(index, 1); - } else if (sensor.new == true && sensor.notValidInput == true) { - sensorsData.splice(index, 1); - } else if (sensor.editing == true) { - delete sensor.editing; - } - } - } - }, [actionData, sensorsData, setToastOpen]); + React.useEffect(() => { + //* if sensors data were updated successfully + if (actionData && actionData?.isUpdated) { + //* show notification when data is successfully updated + setToastOpen(true) + // window.location.reload(); + //* reset sensor data elements + for (let index = 0; index < sensorsData.length; index++) { + const sensor = sensorsData[index] + if (sensor.new == true && sensor.notValidInput == true) { + sensorsData.splice(index, 1) + } else if (sensor.deleted) { + sensorsData.splice(index, 1) + } else if (sensor.new == true && sensor.notValidInput == true) { + sensorsData.splice(index, 1) + } else if (sensor.editing == true) { + delete sensor.editing + } + } + } + }, [actionData, sensorsData, setToastOpen]) - return ( -
    - {/* sensor form */} -
    -
    - {/* Form */} -
    - {/* Heading */} -
    - {/* Title */} -
    -
    -

    Sensor

    -
    -
    - {/* Add button */} - - {/* Save button */} - -
    -
    -
    + return ( +
    + {/* sensor form */} +
    +
    + {/* Form */} + + {/* Heading */} +
    + {/* Title */} +
    +
    +

    Sensor

    +
    +
    + {/* Add button */} + + {/* Save button */} + +
    +
    +
    - {/* divider */} -
    + {/* divider */} +
    -
    -

    - Data measured by sensors that you are going to delete will be - deleted as well. If you add new sensors, don't forget to - retrieve your new script (see tab 'Script'). -

    -
    +
    +

    + Data measured by sensors that you are going to delete will be + deleted as well. If you add new sensors, don't forget to + retrieve your new script (see tab 'Script'). +

    +
    -
      - {sensorsData?.map((sensor: any, index: number) => { - return ( -
    • -
      - {/* left side -> sensor icons list */} -
      - {sensor?.editing ? ( - -
      - {/* view icon */} - +
        + {sensorsData?.map((sensor: any, index: number) => { + return ( +
      • +
        + {/* left side -> sensor icons list */} +
        + {sensor?.editing ? ( + +
        + {/* view icon */} + - {/* down arrow icon */} - - - - - - - {iconsList?.map((icon: any) => { - const Icon = icon.name; - return ( - { - setTepmState(!tepmState); - sensor.icon = icon.id; - }} - > - - - ); - })} - - - -
        -
        - ) : ( - - {sensor.icon - ? getIcon(sensor.icon) - : assignIcon(sensor.sensorType, sensor.title)} - - )} -
        - {/* middle -> sensor attributes */} -
        - {/* shown by default */} - {!sensor?.editing && ( - - - Phenomenon: - - {sensor?.title} - - - ID: - - {sensor?.id} - - - - Unit: - - {sensor?.unit} - - - - Type: - - {sensor?.sensorType} - - - - )} + {/* down arrow icon */} + + + + + + + {iconsList?.map((icon: any) => { + const Icon = icon.name + return ( + { + setTepmState(!tepmState) + sensor.icon = icon.id + }} + > + + + ) + })} + + + +
        + + ) : ( + + {sensor.icon + ? getIcon(sensor.icon) + : assignIcon(sensor.sensorType, sensor.title)} + + )} +
        + {/* middle -> sensor attributes */} +
        + {/* shown by default */} + {!sensor?.editing && ( + + + Phenomenon: + + {sensor?.title} + + + ID: + + {sensor?.id} + + + + Unit: + + {sensor?.unit} + + + + Type: + + {sensor?.sensorType} + + + + )} - {/* shown when edit button clicked */} - {sensor?.editing && ( -
        -
        - - { - setTepmState(!tepmState); - sensor.title = e.target.value; - if (sensor.title.length === 0) { - sensor.notValidInput = true; - } else { - sensor.notValidInput = false; - } - }} - /> -
        -
        - - { - setTepmState(!tepmState); - sensor.sensorType = e.target.value; - if (sensor.sensorType.length === 0) { - sensor.notValidInput = true; - } else { - sensor.notValidInput = false; - } - }} - /> -
        -
        - - { - setTepmState(!tepmState); - sensor.unit = e.target.value; - if (sensor.unit.length === 0) { - sensor.notValidInput = true; - } else { - sensor.notValidInput = false; - } - }} - /> -
        -
        - )} -
        + {/* shown when edit button clicked */} + {sensor?.editing && ( +
        +
        + + { + setTepmState(!tepmState) + sensor.title = e.target.value + if (sensor.title.length === 0) { + sensor.notValidInput = true + } else { + sensor.notValidInput = false + } + }} + /> +
        +
        + + { + setTepmState(!tepmState) + sensor.sensorType = e.target.value + if (sensor.sensorType.length === 0) { + sensor.notValidInput = true + } else { + sensor.notValidInput = false + } + }} + /> +
        +
        + + { + setTepmState(!tepmState) + sensor.unit = e.target.value + if (sensor.unit.length === 0) { + sensor.notValidInput = true + } else { + sensor.notValidInput = false + } + }} + /> +
        +
        + )} +
      - {/* right side -> Save, delete, cancel buttons */} -
      - {/* buttons shown by default */} - - {/* warning text - delete */} - {sensor?.deleting && ( - - This sensor will be deleted. - - )} + {/* right side -> Save, delete, cancel buttons */} +
      + {/* buttons shown by default */} + + {/* warning text - delete */} + {sensor?.deleting && ( + + This sensor will be deleted. + + )} - {/* undo button */} - {sensor?.deleting && ( - - )} + {/* undo button */} + {sensor?.deleting && ( + + )} - {!sensor?.editing && !sensor?.deleting && ( - - {/* edit button */} - {/* ToDo: why onClick not updating the state unless dummy unrelated state is updated */} - + {!sensor?.editing && !sensor?.deleting && ( + + {/* edit button */} + {/* ToDo: why onClick not updating the state unless dummy unrelated state is updated */} + - {/* delete button */} - - - )} - + {/* delete button */} + + + )} + - {sensor?.editing && ( - - {/* invalid input text */} - {sensor?.notValidInput && ( - - Please fill out all required fields. - - )} + {sensor?.editing && ( + + {/* invalid input text */} + {sensor?.notValidInput && ( + + Please fill out all required fields. + + )} - {/* save button */} - + {/* save button */} + - {/* cancel button */} - - - )} -
      -
      -
    • - ); - })} -
    + {/* cancel button */} + + + )} +
    +
    + + ) + })} + - {/* As there's no way to send data wiht form on submit to action (see: https://github.com/remix-run/react-router/discussions/10264) */} - - -
    -
    -
    - ); + {/* As there's no way to send data wiht form on submit to action (see: https://github.com/remix-run/react-router/discussions/10264) */} + + +
    +
    +
    + ) } export function ErrorBoundary() { - return ( -
    - -
    - ); + return ( +
    + +
    + ) } diff --git a/app/routes/device.$deviceId.edit.transfer.tsx b/app/routes/device.$deviceId.edit.transfer.tsx index 42e71129..16e45e39 100644 --- a/app/routes/device.$deviceId.edit.transfer.tsx +++ b/app/routes/device.$deviceId.edit.transfer.tsx @@ -1,127 +1,125 @@ -import { Info } from "lucide-react"; -import { type LoaderFunctionArgs, redirect , Form } from "react-router"; -import ErrorMessage from "~/components/error-message"; -import { getUserId } from "~/utils/session.server"; +import { Info } from 'lucide-react' +import { type LoaderFunctionArgs, redirect, Form } from 'react-router' +import ErrorMessage from '~/components/error-message' +import { getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') - return ""; + return '' } //***************************************************** export async function action() { - return ""; + return '' } //********************************** export default function EditBoxTransfer() { - return ( -
    -
    -
    - {/* Form */} -
    - {/* Heading */} -
    - {/* Title */} -
    -
    -

    Transfer

    -
    -
    -
    + return ( +
    +
    +
    + {/* Form */} + + {/* Heading */} +
    + {/* Title */} +
    +
    +

    Transfer

    +
    +
    +
    - {/* divider */} -
    + {/* divider */} +
    -
    -

    - - Transfer this device to another user! -

    -
    -

    - To perform the transfer, enter the name below and click the - button. A token will be displayed. You pass this{" "} - token to the new owner. The new owner has to enter the - token in his account and click on Claim device. After - that the device will be transferred to the new account. -
    -
    - The transfer may be delayed until the new owner has entered the{" "} - token. -

    -
    +
    +

    + + Transfer this device to another user! +

    +
    +

    + To perform the transfer, enter the name below and click the + button. A token will be displayed. You pass this{' '} + token to the new owner. The new owner has to enter the + token in his account and click on Claim device. After + that the device will be transferred to the new account. +
    +
    + The transfer may be delayed until the new owner has entered the{' '} + token. +

    +
    - {/* Expiration */} -
    - + {/* Expiration */} +
    + -
    - -
    -
    +
    + +
    +
    - {/* Type */} -
    - + {/* Type */} +
    + -
    - -
    -
    +
    + +
    +
    - {/* Transfer button */} - - -
    -
    -
    - ); + {/* Transfer button */} + + +
    +
    +
    + ) } export function ErrorBoundary() { - return ( -
    - -
    - ); + return ( +
    + +
    + ) } diff --git a/app/routes/device.$deviceId.edit.tsx b/app/routes/device.$deviceId.edit.tsx index 4e92728d..c221fd4d 100644 --- a/app/routes/device.$deviceId.edit.tsx +++ b/app/routes/device.$deviceId.edit.tsx @@ -1,173 +1,178 @@ - //* Toast impl. -import * as ToastPrimitive from "@radix-ui/react-toast"; -import { clsx } from "clsx"; +import * as ToastPrimitive from '@radix-ui/react-toast' +import { clsx } from 'clsx' +import { + ArrowRightLeft, + Lock, + MapPin, + FileText, + Wifi, + Sheet, + Cpu, + ArrowLeft, + UploadCloud, + NotepadText, +} from 'lucide-react' +import { useState } from 'react' import { - ArrowRightLeft, - Lock, - MapPin, - FileText, - Wifi, - Sheet, - Cpu, - ArrowLeft, - UploadCloud, - NotepadText, -} from "lucide-react"; -import { useState } from "react"; -import { redirect , Link, Outlet, useParams, type LoaderFunctionArgs } from "react-router"; -import ErrorMessage from "~/components/error-message"; -import { EditDviceSidebarNav } from "~/components/mydevices/edit-device/edit-device-sidebar-nav"; -import { NavBar } from "~/components/nav-bar"; -import { Separator } from "~/components/ui/separator"; -import { getUserId } from "~/utils/session.server"; + redirect, + Link, + Outlet, + useParams, + type LoaderFunctionArgs, +} from 'react-router' +import ErrorMessage from '~/components/error-message' +import { EditDviceSidebarNav } from '~/components/mydevices/edit-device/edit-device-sidebar-nav' +import { NavBar } from '~/components/nav-bar' +import { Separator } from '~/components/ui/separator' +import { getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request, params }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') - const deviceID = params.deviceId; + const deviceID = params.deviceId - return { DevieID: deviceID }; + return { DevieID: deviceID } } //***************************************************** export async function action() { - return redirect("/"); + return redirect('/') } //********************************** export default function EditBox() { - //* Toast notification when device info is updated - const [toastOpen, setToastOpen] = useState(false); + //* Toast notification when device info is updated + const [toastOpen, setToastOpen] = useState(false) - // Get deviceId from route path - const { deviceId } = useParams(); + // Get deviceId from route path + const { deviceId } = useParams() - const sidebarNavItems = [ - { - title: "General", - href: `/device/${deviceId}/edit/general`, - icon: Sheet, - }, - { - title: "Sensors", - href: `/device/${deviceId}/edit/sensors`, - icon: Cpu, - }, - { - title: "Location", - href: `/device/${deviceId}/edit/location`, - icon: MapPin, - }, - { title: "Logs", href: `/device/${deviceId}/edit/logs`, icon: NotepadText }, - { - title: "Security", - href: `/device/${deviceId}/edit/security`, - icon: Lock, - }, - { - title: "Script", - href: `/device/${deviceId}/edit/script`, - icon: FileText, - }, - { - title: "MQTT", - href: `/device/${deviceId}/edit/mqtt`, - icon: Wifi, - }, - { - title: "TTN", - href: `/device/${deviceId}/edit/ttn`, - icon: UploadCloud, - }, - { - title: "Transfer", - href: `/device/${deviceId}/edit/transfer`, - icon: ArrowRightLeft, - }, - ]; + const sidebarNavItems = [ + { + title: 'General', + href: `/device/${deviceId}/edit/general`, + icon: Sheet, + }, + { + title: 'Sensors', + href: `/device/${deviceId}/edit/sensors`, + icon: Cpu, + }, + { + title: 'Location', + href: `/device/${deviceId}/edit/location`, + icon: MapPin, + }, + { title: 'Logs', href: `/device/${deviceId}/edit/logs`, icon: NotepadText }, + { + title: 'Security', + href: `/device/${deviceId}/edit/security`, + icon: Lock, + }, + { + title: 'Script', + href: `/device/${deviceId}/edit/script`, + icon: FileText, + }, + { + title: 'MQTT', + href: `/device/${deviceId}/edit/mqtt`, + icon: Wifi, + }, + { + title: 'TTN', + href: `/device/${deviceId}/edit/ttn`, + icon: UploadCloud, + }, + { + title: 'Transfer', + href: `/device/${deviceId}/edit/transfer`, + icon: ArrowRightLeft, + }, + ] - return ( -
    - + return ( +
    + - {/*Toast notification */} -
    - - -
    -
    -
    - - {/* Account successfully deleted. */} -
    - device succesfully updated - - - {" "} - - view - {" "} - -
    + {/*Toast notification */} +
    + + +
    +
    +
    + + {/* Account successfully deleted. */} +
    + device succesfully updated - + + {' '} + + view + {' '} + +
    - - × - -
    -
    -
    -
    -
    - -
    -
    + + × + +
    +
    +
    +
    +
    + +
    +
    -
    - - Back to Dashboard -
    +
    + + Back to Dashboard +
    -
    -

    Device settings

    -

    Manage your device data.

    -
    - -
    - {/*
    */} - -
    - -
    -
    -
    - ); +
    +

    Device settings

    +

    Manage your device data.

    +
    + +
    + {/*
    */} + +
    + +
    +
    +
    + ) } export function ErrorBoundary() { - return ( -
    - -
    - ); + return ( +
    + +
    + ) } diff --git a/app/routes/device.$deviceId.edit.ttn.tsx b/app/routes/device.$deviceId.edit.ttn.tsx index a5785e94..ce83ba49 100644 --- a/app/routes/device.$deviceId.edit.ttn.tsx +++ b/app/routes/device.$deviceId.edit.ttn.tsx @@ -1,186 +1,186 @@ -import { Save } from "lucide-react"; -import { type LoaderFunctionArgs, redirect , Form } from "react-router"; -import ErrorMessage from "~/components/error-message"; -import { getUserId } from "~/utils/session.server"; +import { Save } from 'lucide-react' +import { type LoaderFunctionArgs, redirect, Form } from 'react-router' +import ErrorMessage from '~/components/error-message' +import { getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') - return ""; + return '' } //***************************************************** export async function action() { - return ""; + return '' } //********************************** export default function EditBoxTTN() { - return ( -
    -
    -
    - {/* Form */} -
    - {/* Heading */} -
    - {/* Title */} -
    -
    -

    TheThingsNetwork - TTN

    -
    -
    - {/* Save button */} - -
    -
    -
    - - {/* divider */} -
    - -
    -

    - openSenseMap offers an integration with{" "} - - TheThingsNetwork.{" "} - - Documentation for the parameters is provided{" "} - - on GitHub - -

    -
    - - {/* Decoding Profile */} -
    - - -
    - -
    -
    - - {/* TTN Application ID */} -
    - - -
    - -
    -
    - - {/* TTN Device ID */} -
    - - -
    - -
    -
    - - {/* Decoding Options */} -
    - - -
    -