diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a8fc9a4af..7cbd6833c 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 8150d9fd1..0e7ad64c2 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 dbc190e62..4115c0826 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 19b9db76b..d5ec743b3 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 d6c65043d..468eed981 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/.prettierignore b/.prettierignore index 5b26b8f97..60bf1ac61 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,5 +5,5 @@ node_modules .env /postgres-data - -/app/styles/**/*.css +/db/seeds/**/* +/drizzle/**/* \ No newline at end of file diff --git a/README.md b/README.md index e54ffe4f4..6c1851606 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 711479998..183267657 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 441096bcb..4653e6ce0 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 10493c26d..7b62dfa3f 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 ba8d12f62..40f4f159d 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 af8a81b44..733c3a629 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 1b09b45ea..c11855af3 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 5eabf15f8..098923d73 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 594d983c4..114675ba7 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 10aadbc36..80c3597ec 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 42066ac58..bd4d5077a 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 ba7019d24..240c8f200 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 b7f844144..0027b64d2 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 42e711290..16e45e39d 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 4e92728dd..c221fd4d5 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 a5785e94e..ce83ba49b 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 */} -
    - - -
    -