From 950a92c56a4de6924259b0cadee21020a2287ca3 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 21 Jan 2026 15:31:34 +0100 Subject: [PATCH 1/6] This commit guides the user to select the appropriate sensor while creating a new device. --- app/components/device/new/sensors-info.tsx | 116 ++++++++++++++++++--- 1 file changed, 104 insertions(+), 12 deletions(-) diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index 1c6b77ed..fe2fa3b0 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -1,8 +1,10 @@ +import { InfoIcon } from 'lucide-react' import { useState, useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { z } from 'zod' import { CustomDeviceConfig } from './custom-device-config' import { Card, CardContent } from '~/components/ui/card' +import { useToast } from '~/components/ui/use-toast' import { cn } from '~/lib/utils' import { getSensorsForModel } from '~/utils/model-definitions' @@ -24,12 +26,14 @@ type SensorGroup = { export function SensorSelectionStep() { const { watch, setValue } = useFormContext() + const { toast } = useToast() const selectedDevice = watch('model') const [selectedDeviceModel, setSelectedDeviceModel] = useState( null, ) const [sensors, setSensors] = useState([]) const [selectedSensors, setSelectedSensors] = useState([]) + const [highlightedGroup, setHighlightedGroup] = useState(null) useEffect(() => { if (selectedDevice) { @@ -38,12 +42,11 @@ export function SensorSelectionStep() { : selectedDevice setSelectedDeviceModel(deviceModel) - if (deviceModel !== 'custom') { + const fetchSensors = () => { const fetchedSensors = getSensorsForModel(deviceModel) setSensors(fetchedSensors) - } else { - setSensors([]) } + fetchSensors() } }, [selectedDevice]) @@ -52,6 +55,27 @@ export function SensorSelectionStep() { setSelectedSensors(savedSelectedSensors) }, [watch]) + // Clear highlight after a delay + useEffect(() => { + if (highlightedGroup) { + const timer = setTimeout(() => { + setHighlightedGroup(null) + }, 2000) + return () => clearTimeout(timer) + } + }, [highlightedGroup]) + + const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { + const grouped = sensors.reduce( + (acc, sensor) => { + if (!acc[sensor.sensorType]) { + acc[sensor.sensorType] = [] + } + acc[sensor.sensorType].push(sensor) + return acc + }, + {} as Record, + ) const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { const grouped = sensors.reduce( (acc, sensor) => { @@ -70,7 +94,14 @@ export function SensorSelectionStep() { image: sensors.find((sensor) => sensor.image)?.image, })) } + return Object.entries(grouped).map(([sensorType, sensors]) => ({ + sensorType, + sensors, + image: sensors.find((sensor) => sensor.image)?.image, + })) + } + const sensorGroups = groupSensorsByType(sensors) const sensorGroups = groupSensorsByType(sensors) const handleGroupToggle = (group: SensorGroup) => { @@ -104,6 +135,26 @@ export function SensorSelectionStep() { setValue('selectedSensors', updatedSensors) } + const handleCardClick = (group: SensorGroup) => { + if (selectedDeviceModel === 'senseBoxHomeV2') { + // For senseBoxHomeV2, clicking the card selects the whole group + handleGroupToggle(group) + } else { + // For other devices, highlight parameters and show info toast + setHighlightedGroup(group.sensorType) + toast({ + title: 'Select Parameters', + description: + 'Click on the individual parameters below to select the sensors you want to use.', + duration: 3000, + }) + } + } + + const handleSensorToggle = (sensor: Sensor) => { + const isAlreadySelected = selectedSensors.some( + (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, + ) const handleSensorToggle = (sensor: Sensor) => { const isAlreadySelected = selectedSensors.some( (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, @@ -119,17 +170,49 @@ export function SensorSelectionStep() { setSelectedSensors(updatedSensors) setValue('selectedSensors', updatedSensors) } + const updatedSensors = isAlreadySelected + ? selectedSensors.filter( + (s) => + !(s.title === sensor.title && s.sensorType === sensor.sensorType), + ) + : [...selectedSensors, sensor] + + setSelectedSensors(updatedSensors) + setValue('selectedSensors', updatedSensors) + } + if (!selectedDevice) { + return

Please select a device first.

+ } if (!selectedDevice) { return

Please select a device first.

} - if (selectedDevice === 'custom') { + if (selectedDevice === 'Custom') { return } + const isSenseBoxHomeV2 = selectedDeviceModel === 'senseBoxHomeV2' + return (
+ {/* Instruction banner */} +
+ {isSenseBoxHomeV2 ? ( + + Click on a sensor card to select all its parameters at once. + + ) : ( + + +

+ Click on individual parameters within each card to select the + sensors you need. +

+
+ )} +
+
{sensorGroups.map((group) => { @@ -140,6 +223,7 @@ export function SensorSelectionStep() { s.sensorType === sensor.sensorType, ), ) + const isHighlighted = highlightedGroup === group.sensorType return ( handleGroupToggle(group) - : undefined - } + onClick={() => handleCardClick(group)} >

+
    + {group.sensors.map((sensor) => { + const isSelected = selectedSensors.some( + (s) => + s.title === sensor.title && + s.sensorType === sensor.sensorType, + )
      {group.sensors.map((sensor) => { const isSelected = selectedSensors.some( @@ -177,13 +265,17 @@ export function SensorSelectionStep() {
    • { e.stopPropagation() handleSensorToggle(sensor) From 07a0977e47a844d1d68292b50c74da81c6018d9e Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 11 Feb 2026 14:01:00 +0100 Subject: [PATCH 2/6] updated sensor-info component to fix the proper closing of tags. --- app/components/device/new/sensors-info.tsx | 56 ++-------------------- 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index fe2fa3b0..9822065a 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -1,4 +1,3 @@ -import { InfoIcon } from 'lucide-react' import { useState, useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { z } from 'zod' @@ -65,17 +64,6 @@ export function SensorSelectionStep() { } }, [highlightedGroup]) - const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { - const grouped = sensors.reduce( - (acc, sensor) => { - if (!acc[sensor.sensorType]) { - acc[sensor.sensorType] = [] - } - acc[sensor.sensorType].push(sensor) - return acc - }, - {} as Record, - ) const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { const grouped = sensors.reduce( (acc, sensor) => { @@ -94,14 +82,7 @@ export function SensorSelectionStep() { image: sensors.find((sensor) => sensor.image)?.image, })) } - return Object.entries(grouped).map(([sensorType, sensors]) => ({ - sensorType, - sensors, - image: sensors.find((sensor) => sensor.image)?.image, - })) - } - const sensorGroups = groupSensorsByType(sensors) const sensorGroups = groupSensorsByType(sensors) const handleGroupToggle = (group: SensorGroup) => { @@ -151,10 +132,6 @@ export function SensorSelectionStep() { } } - const handleSensorToggle = (sensor: Sensor) => { - const isAlreadySelected = selectedSensors.some( - (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, - ) const handleSensorToggle = (sensor: Sensor) => { const isAlreadySelected = selectedSensors.some( (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, @@ -170,20 +147,7 @@ export function SensorSelectionStep() { setSelectedSensors(updatedSensors) setValue('selectedSensors', updatedSensors) } - const updatedSensors = isAlreadySelected - ? selectedSensors.filter( - (s) => - !(s.title === sensor.title && s.sensorType === sensor.sensorType), - ) - : [...selectedSensors, sensor] - - setSelectedSensors(updatedSensors) - setValue('selectedSensors', updatedSensors) - } - if (!selectedDevice) { - return

      Please select a device first.

      - } if (!selectedDevice) { return

      Please select a device first.

      } @@ -197,18 +161,15 @@ export function SensorSelectionStep() { return (
      {/* Instruction banner */} -
      +
      {isSenseBoxHomeV2 ? ( Click on a sensor card to select all its parameters at once. ) : ( - - -

      - Click on individual parameters within each card to select the - sensors you need. -

      + + Click on individual parameters within each card to select the + sensors you need. )}
      @@ -233,7 +194,7 @@ export function SensorSelectionStep() { isGroupSelected ? 'shadow-lg ring-2 ring-primary' : 'hover:shadow-md', - isHighlighted && 'ring-blue-400 bg-blue-50 ring-2', + isHighlighted && 'bg-blue-50 ring-2 ring-blue-400', )} onClick={() => handleCardClick(group)} > @@ -246,13 +207,6 @@ export function SensorSelectionStep() { {group.sensorType}

-
    - {group.sensors.map((sensor) => { - const isSelected = selectedSensors.some( - (s) => - s.title === sensor.title && - s.sensorType === sensor.sensorType, - )
      {group.sensors.map((sensor) => { const isSelected = selectedSensors.some( From 7215ccbbaffdb73b01d928390cf1ef7f1fa988c6 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 11 Feb 2026 15:57:27 +0100 Subject: [PATCH 3/6] This commit will change the card appearance of the sensors to an accordion based display, that enables the users to select the parameters clicking the checkboxes associated with them. --- app/components/device/new/sensors-info.tsx | 323 +++++++++++---------- 1 file changed, 172 insertions(+), 151 deletions(-) diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index 9822065a..422f82ad 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -2,8 +2,15 @@ import { useState, useEffect } from 'react' import { useFormContext } from 'react-hook-form' import { z } from 'zod' import { CustomDeviceConfig } from './custom-device-config' -import { Card, CardContent } from '~/components/ui/card' -import { useToast } from '~/components/ui/use-toast' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '~/components/ui/accordion' +import { Badge } from '~/components/ui/badge' +import { Checkbox } from '~/components/ui/checkbox' +import { Label } from '~/components/ui/label' import { cn } from '~/lib/utils' import { getSensorsForModel } from '~/utils/model-definitions' @@ -25,14 +32,12 @@ type SensorGroup = { export function SensorSelectionStep() { const { watch, setValue } = useFormContext() - const { toast } = useToast() const selectedDevice = watch('model') const [selectedDeviceModel, setSelectedDeviceModel] = useState( null, ) const [sensors, setSensors] = useState([]) const [selectedSensors, setSelectedSensors] = useState([]) - const [highlightedGroup, setHighlightedGroup] = useState(null) useEffect(() => { if (selectedDevice) { @@ -41,11 +46,8 @@ export function SensorSelectionStep() { : selectedDevice setSelectedDeviceModel(deviceModel) - const fetchSensors = () => { - const fetchedSensors = getSensorsForModel(deviceModel) - setSensors(fetchedSensors) - } - fetchSensors() + const fetchedSensors = getSensorsForModel(deviceModel) + setSensors(fetchedSensors) } }, [selectedDevice]) @@ -54,16 +56,6 @@ export function SensorSelectionStep() { setSelectedSensors(savedSelectedSensors) }, [watch]) - // Clear highlight after a delay - useEffect(() => { - if (highlightedGroup) { - const timer = setTimeout(() => { - setHighlightedGroup(null) - }, 2000) - return () => clearTimeout(timer) - } - }, [highlightedGroup]) - const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { const grouped = sensors.reduce( (acc, sensor) => { @@ -85,64 +77,50 @@ export function SensorSelectionStep() { const sensorGroups = groupSensorsByType(sensors) - const handleGroupToggle = (group: SensorGroup) => { - const isGroupSelected = group.sensors.every((sensor) => - selectedSensors.some( - (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, - ), + const isSensorSelected = (sensor: Sensor) => + selectedSensors.some( + (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, ) - const updatedSensors = isGroupSelected + const isGroupFullySelected = (group: SensorGroup) => + group.sensors.every((sensor) => isSensorSelected(sensor)) + + const isGroupPartiallySelected = (group: SensorGroup) => + group.sensors.some((sensor) => isSensorSelected(sensor)) && + !isGroupFullySelected(group) + + const getSelectedCountForGroup = (group: SensorGroup) => + group.sensors.filter((sensor) => isSensorSelected(sensor)).length + + const handleSensorToggle = (sensor: Sensor) => { + const isAlreadySelected = isSensorSelected(sensor) + + const updatedSensors = isAlreadySelected ? selectedSensors.filter( (s) => - !group.sensors.some( - (sensor) => - s.title === sensor.title && s.sensorType === sensor.sensorType, - ), + !(s.title === sensor.title && s.sensorType === sensor.sensorType), ) - : [ - ...selectedSensors, - ...group.sensors.filter( - (sensor) => - !selectedSensors.some( - (s) => - s.title === sensor.title && - s.sensorType === sensor.sensorType, - ), - ), - ] + : [...selectedSensors, sensor] setSelectedSensors(updatedSensors) setValue('selectedSensors', updatedSensors) } - const handleCardClick = (group: SensorGroup) => { - if (selectedDeviceModel === 'senseBoxHomeV2') { - // For senseBoxHomeV2, clicking the card selects the whole group - handleGroupToggle(group) - } else { - // For other devices, highlight parameters and show info toast - setHighlightedGroup(group.sensorType) - toast({ - title: 'Select Parameters', - description: - 'Click on the individual parameters below to select the sensors you want to use.', - duration: 3000, - }) - } - } - - const handleSensorToggle = (sensor: Sensor) => { - const isAlreadySelected = selectedSensors.some( - (s) => s.title === sensor.title && s.sensorType === sensor.sensorType, - ) + const handleGroupToggle = (group: SensorGroup) => { + const isFullySelected = isGroupFullySelected(group) - const updatedSensors = isAlreadySelected + const updatedSensors = isFullySelected ? selectedSensors.filter( (s) => - !(s.title === sensor.title && s.sensorType === sensor.sensorType), + !group.sensors.some( + (sensor) => + s.title === sensor.title && s.sensorType === sensor.sensorType, + ), ) - : [...selectedSensors, sensor] + : [ + ...selectedSensors, + ...group.sensors.filter((sensor) => !isSensorSelected(sensor)), + ] setSelectedSensors(updatedSensors) setValue('selectedSensors', updatedSensors) @@ -159,104 +137,147 @@ export function SensorSelectionStep() { const isSenseBoxHomeV2 = selectedDeviceModel === 'senseBoxHomeV2' return ( -
      - {/* Instruction banner */} -
      - {isSenseBoxHomeV2 ? ( - - Click on a sensor card to select all its parameters at once. - - ) : ( - - Click on individual parameters within each card to select the - sensors you need. - +
      + {/* Selected count summary */} +
      +

      + {selectedSensors.length} sensor + {selectedSensors.length !== 1 ? 's' : ''} selected +

      + {selectedSensors.length > 0 && ( + )}
      -
      -
      - {sensorGroups.map((group) => { - const isGroupSelected = group.sensors.every((sensor) => - selectedSensors.some( - (s) => - s.title === sensor.title && - s.sensorType === sensor.sensorType, - ), - ) - const isHighlighted = highlightedGroup === group.sensorType - - return ( - handleCardClick(group)} - > - -

      - {group.sensorType} -

      - -
        - {group.sensors.map((sensor) => { - const isSelected = selectedSensors.some( - (s) => - s.title === sensor.title && - s.sensorType === sensor.sensorType, - ) + + {sensorGroups.map((group) => { + const isFullySelected = isGroupFullySelected(group) + const isPartiallySelected = isGroupPartiallySelected(group) + const selectedCount = getSelectedCountForGroup(group) - return ( -
      • { - e.stopPropagation() - handleSensorToggle(sensor) - } - : undefined - } - > - {sensor.title} ({sensor.unit}) -
      • - ) - })} -
      -
      + return ( + + +
      +
      {group.image && ( {`${group.sensorType} )} +
      +

      {group.sensorType}

      +

      + {group.sensors.length} parameter + {group.sensors.length !== 1 ? 's' : ''} +

      +
      - - - ) - })} -
      -
      + {selectedCount > 0 && ( + + {selectedCount} selected + + )} +
      + + +
      + {/* Select All option */} +
      e.stopPropagation()} + > + handleGroupToggle(group)} + /> + +
      + + {/* Individual sensors - only show for non-senseBoxHomeV2 or always show */} + {!isSenseBoxHomeV2 && ( +
      + {group.sensors.map((sensor) => { + const isSelected = isSensorSelected(sensor) + const sensorId = `sensor-${group.sensorType}-${sensor.title}` + + return ( +
      e.stopPropagation()} + > + handleSensorToggle(sensor)} + /> + +
      + ) + })} +
      + )} + + {/* For senseBoxHomeV2, just show the parameters as info */} + {isSenseBoxHomeV2 && ( +
      +

      Includes:

      + {group.sensors.map((sensor) => ( +

      + • {sensor.title} ({sensor.unit}) +

      + ))} +
      + )} +
      +
      + + ) + })} +
      ) } From 8af73ff5e2441bb10c4b31768f5c2a477e436769 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 18 Feb 2026 10:06:45 +0100 Subject: [PATCH 4/6] This commit adds safety check for the custom devices. --- app/components/device/new/sensors-info.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index 422f82ad..37e79e53 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -46,8 +46,13 @@ export function SensorSelectionStep() { : selectedDevice setSelectedDeviceModel(deviceModel) - const fetchedSensors = getSensorsForModel(deviceModel) - setSensors(fetchedSensors) + // Add safety check for custom devices + if (deviceModel !== 'custom') { + const fetchedSensors = getSensorsForModel(deviceModel) + setSensors(fetchedSensors ?? []) // Also handle null return + } else { + setSensors([]) + } } }, [selectedDevice]) @@ -57,6 +62,9 @@ export function SensorSelectionStep() { }, [watch]) const groupSensorsByType = (sensors: Sensor[]): SensorGroup[] => { + // Add safety check in case sensors is null/undefined + if (!sensors || sensors.length === 0) return [] + const grouped = sensors.reduce( (acc, sensor) => { if (!acc[sensor.sensorType]) { @@ -130,7 +138,7 @@ export function SensorSelectionStep() { return

      Please select a device first.

      } - if (selectedDevice === 'Custom') { + if (selectedDevice === 'custom') { return } @@ -209,7 +217,6 @@ export function SensorSelectionStep() {
      - {/* Individual sensors - only show for non-senseBoxHomeV2 or always show */} + {/* Individual sensors - only show for non-senseBoxHomeV2 */} {!isSenseBoxHomeV2 && (
      {group.sensors.map((sensor) => { @@ -280,4 +287,4 @@ export function SensorSelectionStep() {
      ) -} +} \ No newline at end of file From 824fab419d3f196326ed2153a79101b53e829e02 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 18 Feb 2026 17:07:05 +0100 Subject: [PATCH 5/6] This commit fixes the issue of selecting multiple sensor phenomenon even though the user selects only specific phenomens. --- app/components/device/new/sensors-info.tsx | 4 ++-- app/models/device.server.ts | 24 ++++++++++++++++------ app/routes/device.new.tsx | 11 +++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index 37e79e53..af6b9540 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -195,7 +195,7 @@ export function SensorSelectionStep() {

      {group.sensorType}

      - {group.sensors.length} parameter + {group.sensors.length} phenomenon {group.sensors.length !== 1 ? 's' : ''}

      @@ -226,7 +226,7 @@ export function SensorSelectionStep() { htmlFor={`group-${group.sensorType}`} className="cursor-pointer font-medium" > - Select all parameters + Select all phenomenons
      diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 36aff181..01a270cf 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -242,7 +242,7 @@ export async function updateDevice( const result = await drizzleClient.transaction(async (tx) => { if (args.location) { - const { lat, lng, height } = args.location + const { lat, lng } = args.location const pointWKT = `POINT(${lng} ${lat})` @@ -723,15 +723,27 @@ export async function createDevice(deviceData: any, userId: string) { } if ( - Array.isArray(deviceData.sensorTemplates) && - deviceData.sensorTemplates.length > 0 + Array.isArray(deviceData.selectedSensorDetails) && + deviceData.selectedSensorDetails.length > 0 ) { sensorsToAdd = modelSensors.filter((sensor) => - deviceData.sensorTemplates.includes( - sensor.sensorType.toLowerCase(), + deviceData.selectedSensorDetails.some( + (selected: { title: string; sensorType: string }) => + selected.title === sensor.title && + selected.sensorType === sensor.sensorType, ), ) - } else { + } + else if (Array.isArray(deviceData.sensorTemplates) && + deviceData.sensorTemplates.length > 0){ + sensorsToAdd = modelSensors.filter((sensor) => + deviceData.sensorTemplates.includes( + sensor.sensorType.toLowerCase(), + ), + ) + } + + else { sensorsToAdd = modelSensors } } diff --git a/app/routes/device.new.tsx b/app/routes/device.new.tsx index 0d294659..7638faa4 100644 --- a/app/routes/device.new.tsx +++ b/app/routes/device.new.tsx @@ -43,10 +43,11 @@ export async function action({ request }: ActionFunctionArgs) { ...(data['device-selection'].model !== 'custom' && { model: data['device-selection'].model, - - sensorTemplates: selectedSensors.map((sensor: any) => - sensor.sensorType.toLowerCase(), - ), + // Send full sensor details for precise filtering + selectedSensorDetails: selectedSensors.map((sensor: any) => ({ + title: sensor.title, + sensorType: sensor.sensorType, + })), }), ...(data['device-selection'].model === 'custom' && { @@ -62,7 +63,6 @@ export async function action({ request }: ActionFunctionArgs) { ttnEnabled: data.advanced.ttnEnabled ?? false, } - // Call server function const newDevice = await createDevice(devicePayload, userId) console.log('🚀 ~ New Device Created:', newDevice) @@ -72,7 +72,6 @@ export async function action({ request }: ActionFunctionArgs) { return redirect('/profile/me') } } - export default function NewDevice() { return (
      From ba3d24b8a56f772d8519c73bb61d993cb7137fbe Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 25 Feb 2026 12:20:09 +0100 Subject: [PATCH 6/6] Adds missing translations. --- app/components/device/new/advanced-info.tsx | 14 +++--- .../device/new/custom-device-config.tsx | 10 ++-- app/components/device/new/general-info.tsx | 16 ++++--- app/components/device/new/location-info.tsx | 12 +++-- .../device/new/new-device-stepper.tsx | 28 ++++++----- app/components/device/new/sensors-info.tsx | 17 ++++--- app/components/device/new/summary-info.tsx | 15 +++--- app/components/nav-bar.tsx | 8 ++-- public/locales/de/navbar.json | 2 + public/locales/de/newdevice.json | 47 +++++++++++++++++-- public/locales/en/navbar.json | 2 + public/locales/en/newdevice.json | 41 ++++++++++++++-- 12 files changed, 152 insertions(+), 60 deletions(-) diff --git a/app/components/device/new/advanced-info.tsx b/app/components/device/new/advanced-info.tsx index 240c8f20..160a5e7c 100644 --- a/app/components/device/new/advanced-info.tsx +++ b/app/components/device/new/advanced-info.tsx @@ -1,4 +1,5 @@ import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Card, CardContent, @@ -20,6 +21,7 @@ import { Textarea } from '~/components/ui/textarea' export function AdvancedStep() { const { register, setValue, watch, resetField } = useFormContext() + const { t } = useTranslation('newdevice') // Watch field states const isMqttEnabled = watch('mqttEnabled') || false @@ -64,15 +66,15 @@ export function AdvancedStep() { {/* MQTT Configuration */} - MQTT Configuration + {t('MQTT Configuration')} - Configure your MQTT settings for data streaming + {t('mqtt_config_text')}
      - TTN Configuration + {t('TTN Configuration')} - Configure your TTN (The Things Network) settings + {t('ttn_config_text')}
      { @@ -53,7 +55,7 @@ export function CustomDeviceConfig() {
      - +
      - +
      - + - Add Sensor + {t('Add Sensor')}
      diff --git a/app/components/device/new/general-info.tsx b/app/components/device/new/general-info.tsx index 3af5395d..65f52fc8 100644 --- a/app/components/device/new/general-info.tsx +++ b/app/components/device/new/general-info.tsx @@ -1,6 +1,7 @@ import { Plus, Cloud, Home, HelpCircle, Bike, X, Info } from 'lucide-react' import React, { useState } from 'react' import { useFormContext, useFieldArray } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '~/components/ui/badge' @@ -13,10 +14,12 @@ import { TooltipTrigger, } from '~/components/ui/tooltip' + type ExposureOption = 'outdoor' | 'indoor' | 'mobile' | 'unknown' export function GeneralInfoStep() { const { register, control, setValue, getValues, watch } = useFormContext() + const {t} = useTranslation('newdevice') const { fields, append, remove } = useFieldArray({ control, name: 'tags', // Tags array @@ -88,7 +91,7 @@ export function GeneralInfoStep() { />
      - +
      {exposureOptions.map((option) => ( ))}
      @@ -117,7 +120,7 @@ export function GeneralInfoStep() { onCheckedChange={handleTemporaryChange} /> @@ -133,8 +136,7 @@ export function GeneralInfoStep() { {

      - Temporary devices will be automatically deleted after a - maximum of one month. + {t('temporary_info_text')}

      }
      @@ -147,7 +149,7 @@ export function GeneralInfoStep() { htmlFor="temporaryExpirationDate" className="whitespace-nowrap text-sm font-medium" > - Expiration Date: + {t('expiration_date')} { if (e.key === 'Enter') { e.preventDefault() diff --git a/app/components/device/new/location-info.tsx b/app/components/device/new/location-info.tsx index dc1681e7..6d2ce582 100644 --- a/app/components/device/new/location-info.tsx +++ b/app/components/device/new/location-info.tsx @@ -1,5 +1,6 @@ import React, { useRef, useState, useEffect, useCallback } from 'react' import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Map, Marker, @@ -12,10 +13,11 @@ import { Input } from '@/components/ui/input' import { Label } from '~/components/ui/label' import 'mapbox-gl/dist/mapbox-gl.css' + export function LocationStep() { const mapRef = useRef(null) const { register, setValue, watch } = useFormContext() - + const { t } = useTranslation('newdevice') const savedLatitude = watch('latitude') const savedLongitude = watch('longitude') @@ -129,7 +131,7 @@ export function LocationStep() {
      - +
      - +
      diff --git a/app/components/device/new/new-device-stepper.tsx b/app/components/device/new/new-device-stepper.tsx index af5f2201..058c0fa9 100644 --- a/app/components/device/new/new-device-stepper.tsx +++ b/app/components/device/new/new-device-stepper.tsx @@ -3,6 +3,7 @@ import { defineStepper } from '@stepperize/react' import { Info, Slash } from 'lucide-react' import { useEffect, useState } from 'react' import { type FieldErrors, FormProvider, useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Form, useSubmit } from 'react-router' import { z } from 'zod' import { AdvancedStep } from './advanced-info' @@ -173,42 +174,42 @@ export const Stepper = defineStepper( { id: 'general-info', label: 'General Info', - info: 'Provide a unique name for your device, select its operating environment (outdoor, indoor, mobile, or unknown), and add relevant tags (optional).', + infoKey: 'general_information_text', schema: generalInfoSchema, index: 0, }, { id: 'location', label: 'Location', - info: "Select the device's location by clicking on the map or entering latitude and longitude coordinates manually. Drag the marker on the map to adjust the location if needed.", + infoKey: 'location_info_text', schema: locationSchema, index: 1, }, { id: 'device-selection', label: 'Device Selection', - info: 'Select a device model from the available options', + infoKey: 'device_selection_info_text', schema: deviceSchema, index: 2, }, { id: 'sensor-selection', label: 'Sensor Selection', - info: 'Select sensors for your device by choosing from predefined groups or individual sensors based on your device model. If using a custom device, configure sensors manually.', + infoKey: 'sensor_selection_info_text', schema: sensorsSchema, index: 3, }, { id: 'advanced', label: 'Advanced', - info: null, + infoKey: null, schema: advancedSchema, index: 4, }, { id: 'summary', label: 'Summary', - info: null, + infoKey: null, schema: z.object({}), index: 5, }, @@ -237,6 +238,7 @@ export default function NewDeviceStepper() { resolver: zodResolver(stepper.current.schema), }) const { toast } = useToast() + const { t } = useTranslation('newdevice') const [isFirst, setIsFirst] = useState(false) useEffect(() => { @@ -302,7 +304,7 @@ export default function NewDeviceStepper() { : 'cursor-pointer text-gray-500 hover:text-black' } `} > - {step.label} + {t(step.label)} @@ -320,10 +322,10 @@ export default function NewDeviceStepper() { {/* Step Header with Info */}

      - Step {stepper.current.index + 1} of {Stepper.steps.length}:{' '} - {stepper.current.label} + {t('step')} {stepper.current.index + 1} {t('of')} {Stepper.steps.length}:{' '} + {t(stepper.current.label)}

      - {stepper.current.info && ( + {stepper.current.infoKey && ( - {stepper.current.info} + {t(stepper.current.infoKey)} )} @@ -362,10 +364,10 @@ export default function NewDeviceStepper() { onClick={stepper.prev} disabled={isFirst} > - Back + {t('back')}
      diff --git a/app/components/device/new/sensors-info.tsx b/app/components/device/new/sensors-info.tsx index af6b9540..f97103a5 100644 --- a/app/components/device/new/sensors-info.tsx +++ b/app/components/device/new/sensors-info.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { z } from 'zod' import { CustomDeviceConfig } from './custom-device-config' import { @@ -14,6 +15,7 @@ import { Label } from '~/components/ui/label' import { cn } from '~/lib/utils' import { getSensorsForModel } from '~/utils/model-definitions' + export const sensorSchema = z.object({ title: z.string(), unit: z.string(), @@ -33,6 +35,7 @@ type SensorGroup = { export function SensorSelectionStep() { const { watch, setValue } = useFormContext() const selectedDevice = watch('model') + const { t } = useTranslation('newdevice'); const [selectedDeviceModel, setSelectedDeviceModel] = useState( null, ) @@ -135,7 +138,7 @@ export function SensorSelectionStep() { } if (!selectedDevice) { - return

      Please select a device first.

      + return

      {t('device_not_selected')}

      } if (selectedDevice === 'custom') { @@ -149,8 +152,8 @@ export function SensorSelectionStep() { {/* Selected count summary */}

      - {selectedSensors.length} sensor - {selectedSensors.length !== 1 ? 's' : ''} selected + {selectedSensors.length} {t('sensor')} + {selectedSensors.length !== 1 ? 's' : ''} {t('selected')}

      {selectedSensors.length > 0 && (
      {selectedCount > 0 && ( - {selectedCount} selected + {selectedCount} {t('selected')} )}
      @@ -226,7 +229,7 @@ export function SensorSelectionStep() { htmlFor={`group-${group.sensorType}`} className="cursor-pointer font-medium" > - Select all phenomenons + {t('Select all phenomenons')}
      @@ -271,7 +274,7 @@ export function SensorSelectionStep() { {/* For senseBoxHomeV2, just show the parameters as info */} {isSenseBoxHomeV2 && (
      -

      Includes:

      +

      {t('Includes')}:

      {group.sensors.map((sensor) => (

      • {sensor.title} ({sensor.unit}) diff --git a/app/components/device/new/summary-info.tsx b/app/components/device/new/summary-info.tsx index 245c9c10..bd2d1651 100644 --- a/app/components/device/new/summary-info.tsx +++ b/app/components/device/new/summary-info.tsx @@ -1,11 +1,14 @@ import { MapPin, Tag, Smartphone, Cpu, Cog } from 'lucide-react' import { useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' + export function SummaryInfo() { const { getValues } = useFormContext() const formData = getValues() + const { t } = useTranslation('newdevice') const sections = [ { @@ -13,7 +16,7 @@ export function SummaryInfo() { icon: , data: [ { label: 'Name', value: formData.name }, - { label: 'Exposure', value: formData.exposure }, + { label: 'exposure', value: formData.exposure }, { label: 'Tags', value: @@ -25,9 +28,9 @@ export function SummaryInfo() { title: 'Location', icon: , data: [ - { label: 'Latitude', value: parseFloat(formData.latitude).toFixed(4) }, + { label: 'latitude', value: parseFloat(formData.latitude).toFixed(4) }, { - label: 'Longitude', + label: 'longitude', value: parseFloat(formData.longitude).toFixed(4), }, ], @@ -65,15 +68,15 @@ export function SummaryInfo() {

      {section.icon}

      - {section.title} + {t(section.title)}

      {section.data.map((item: any, idx: any) => (
      - {item.label}: + {t(item.label)}: - {item.value} + {t(item.value)}
      ))} diff --git a/app/components/nav-bar.tsx b/app/components/nav-bar.tsx index 5f02e348..3278b3e1 100644 --- a/app/components/nav-bar.tsx +++ b/app/components/nav-bar.tsx @@ -1,8 +1,8 @@ import { LogIn, Mailbox, Plus } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { Link, useLocation } from 'react-router' import Menu from './header/menu' import { Button } from './ui/button' - import { DropdownMenu, DropdownMenuGroup, @@ -11,7 +11,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useOptionalUser } from '~/utils' -import { useTranslation } from 'react-i18next' + export function NavBar() { const { t } = useTranslation('navbar') @@ -61,13 +61,13 @@ export function NavBar() { - New device + {t("new Device")} - Transfer device + {t("transfer Device")} diff --git a/public/locales/de/navbar.json b/public/locales/de/navbar.json index e8dff9a2..402eee4a 100644 --- a/public/locales/de/navbar.json +++ b/public/locales/de/navbar.json @@ -1,5 +1,7 @@ { "date_picker_label": "Wähle einen Zeitpunkt", + "new Device": "Neues Gerät", + "transfer Device": "Gerät übertragen", "date_range_picker_label": "Wähle einen Zeitraum", "search_label": "Suche", "temperature_label": "Temperatur", diff --git a/public/locales/de/newdevice.json b/public/locales/de/newdevice.json index 96918dc1..4aa0f911 100644 --- a/public/locales/de/newdevice.json +++ b/public/locales/de/newdevice.json @@ -3,36 +3,72 @@ "prev": "Zurück", "next": "Weiter", "submit": "Abschicken", + "step": "Schritt", + "of": "von", + "back": "Zurück", + "Complete": "Abschließen", "select_device": "Gerät auswählen", + "Device": "Gerät", "select_device_text": "Welche Hardware benutzt du?", + "device_not_selected": "Bitte wähle zuerst ein Gerät aus.", + "device_selection_info_text": "Wähle ein Gerätemodell aus den verfügbaren Optionen aus", + "Device Selection": "Geräteauswahl", "own_device": "Eigenes Gerät", "select_device_info_text": "Dein Gerät is nicht in der Liste? Füge es im <0>sensorWiki hinzu, um es auf der openSenseMap zu benutzen.", "general": "Allgemein", + "General Info": "Allgemeine Informationen", + "Location": "Standort", + "Advanced": "Erweitert", + "Summary": "Zusammenfassung", "general_text": "Bitte trage zusätzliche Informationen zu deinem Gerät ein.", + "general_information_text": "Gib einen eindeutigen Namen für dein Gerät an, wähle die Umgebung aus, in der es betrieben wird (draußen, drinnen, mobil oder unbekannt) und füge relevante Tags hinzu (optional).", "general_info_text": "Diese Informationen werden veröffentlicht, also sei vorsichtig beim Teilen. Du kannst die Informationen auch nach der Registrierung noch ändern.", "station_name": "Name deiner Station", "exposure": "Standort", + "selected": "Ausgewählt", + "Select all phenomenons": "Alle Phänomene auswählen", "exposure_explaination": "Der Standort deiner Station.", - "indoor": "drinnen", - "outdoor": "draußen", - "mobile": "mobil", + "Indoor": "drinnen", + "Outdoor": "draußen", + "Mobile": "mobil", + "Unknown": "unbekannt", + "temporary": "temporär Gerät", + "temporary_info_text": "temporäre Geräte werden automatisch nach maximal einem Monat gelöscht.", + "expiration_date": "Ablaufdatum:", + "add_tag": "Tag hinzufügen", "optional": "optional", "select_sensors": "Sensoren auswählen", + "Sensor Selection": "Sensorauswahl", + "Add Sensor": "Sensor hinzufügen", + "sensor_selection_info_text": "Wähle die Sensoren für dein Gerät aus, indem du vordefinierte Gruppen oder einzelne Sensoren basierend auf deinem Gerätemodell auswählst. Wenn du ein benutzerdefiniertes Gerät verwendest, konfiguriere die Sensoren manuell.", "select_sensors_text": "Wähle die Sensoren aus die du benutzen willst. Du kannst einen Sensor mehrfach hinzufügen.", "select_sensors_info_text": "Dein Sensor is nicht in der Liste? Füge ihn im <0>sensorWiki hinzu, um ihn auf der openSenseMap zu benutzen.", "your_added": "Deine hinzugefügten", - "sensors": "Sensoren", + "Sensors": "Sensoren", + "Model": "Modell", "sensor": "Sensor", "phenomenon": "Phänomen", + "MQTT Enabled": "MQTT aktiviert", + "TTN Enabled": "TTN aktiviert", + "Yes": "Ja", + "No": "Nein", "unit": "Einheit", + "type": "Typ", + "Includes": "Enthält", "delete": "Löschen", "title": "Titel", "advanced": "Advanced", "advanced_text": "Erweiterte Konnektivitätseinstellungen für dein Gerät. Du kannst nur eine Option auswählen.", + "MQTT Configuration": "MQTT Konfiguration", + "mqtt_config_text": "Konfiguriere deine MQTT Einstellungen für den Datenstream", + "Enable MQTT": "MQTT aktivieren", + "Enable TTN": "TTN aktivieren", + "TTN Configuration": "TTN Konfiguration", + "ttn_config_text": "Konfiguriere deine TTN (The Things Network) Einstellungen", "ttn_info_text": "Die openSenseMap bietet eine Integration mit <0>TheThingsNetwork an. Eine Dokumentation für die Parameter findest du auf <1>GitHub", "ttn_app_id_info": "Die von TTN erhaltene Application-ID", "ttn_dev_id_info": "Die von TTN erhaltene Device-ID", @@ -48,10 +84,13 @@ "mqtt_connect_options_info": "Eine json-kodierte Zeichenkette mit Optionen, die an den MQTT-Client übergeben werden", "location": "Standort", + "location_info_text": "Wähle den Standort des Geräts aus, indem du auf die Karte klickst oder die Breiten- und Längengradkoordinaten manuell eingibst. Ziehe den Marker auf der Karte, um den Standort bei Bedarf anzupassen.", "location_text": "Klicke in die Karte, um einen Standort für dein Gerät auszuwählen. Du kannst auch Koordinaten manuell eingeben oder die Geosuche benutzen.", "search_placeholder": "Suche", "latitude": "Breitengrad", "longitude": "Längengrad", + "enter latitude": "Breitengrad eingeben (-90 bis 90)", + "enter longitude": "Längengrad eingeben (-180 bis 180)", "height": "Höhe", "height_info_text": "Höhe über dem Meeresspiegel des von Dir gewählten Standorts in Metern. Wenn Du dein Gerät deutlich über der Höhe des Erdbodens aufgestellt hast (z.B. hohes Gebäude), sollten Du diese Höhe zu der abgeleiteten Höhe hinzuaddieren. Die Höhe ist vor allem dann wichtig, wenn Du einen Sensor angeschlossen hast, der den Luftdruck misst, um diese Messungen vergleichbar zu machen.", diff --git a/public/locales/en/navbar.json b/public/locales/en/navbar.json index 083aeae6..8098bfd5 100644 --- a/public/locales/en/navbar.json +++ b/public/locales/en/navbar.json @@ -1,5 +1,7 @@ { "date_picker_label": "Select a date", + "new Device":"New Device", + "transfer Device": "Transfer Device", "date_range_picker_label": "Select a time period", "search_label": "Search", "temperature_label": "Temperature", diff --git a/public/locales/en/newdevice.json b/public/locales/en/newdevice.json index a451a6f9..652eacfa 100644 --- a/public/locales/en/newdevice.json +++ b/public/locales/en/newdevice.json @@ -3,37 +3,67 @@ "prev": "Previous", "next": "Next", "submit": "Submit", + "step": "Step", + "of": "of", + "back": "Back", + "Complete": "Complete", "select_device": "Select Device", + "Device": "Device", "select_device_text": "Which hardware do you use?", + "device_not_selected": "Please select a device first.", + "device_selection_info_text": "Select a device model from the available options", "own_device": "Custom Device", "select_device_info_text": "Your device is not in the list? Add it to the <0>sensorWiki to use it on the openSenseMap.", "general": "General", "general_text": "Please submit additional information about your device.", + "general_information_text": "Provide a unique name for your device, select its operating environment (outdoor, indoor, mobile, or unknown), and add relevant tags (optional).", "general_info_text": "This information will be displayed publicly so be careful what you share. You can change this information later on.", "station_name": "Name of your station", "exposure": "Exposure", + "selected": "Selected", + "Select all phenomenons": "Select all phenomenons", "exposure_explaination": "This is how your device is exposed/placed.", - "indoor": "Indoor", - "outdoor": "Outdoor", - "mobile": "Mobile", + "Indoor": "Indoor", + "Outdoor": "Outdoor", + "Mobile": "Mobile", + "Unknown": "Unknown", + "temporary": "Temporary Device", + "temporary_info_text": "Temporary devices will be automatically deleted after a maximum of one month.", + "expiration_date": "Expiration Date:", + "add_tag": "Add a tag", "optional": "optional", "select_sensors": "Select sensors", + "Add Sensor": "Add Sensor", + "sensor_selection_info_text": "Select sensors for your device by choosing from predefined groups or individual sensors based on your device model. If using a custom device, configure sensors manually.", "select_sensors_text": "Select the sensors you want to use by clicking on the cards. You can add the same sensor multiple times.", "select_sensors_info_text": "Your sensor is not in the list? Add it to the <0>sensorWiki to use it on the openSenseMap.", "your_added": "Your added", - "sensors": "Sensors", + "Sensors": "Sensors", + "Model": "Model", "sensor": "Sensor", "phenomenon": "Phenomenon", + "MQTT Enabled": "MQTT Enabled", + "TTN Enabled": "TTN Enabled", + "Yes": "Yes", + "No": "No", "unit": "Unit", + "type": "Type", + "Includes": "Includes", "delete": "Delete", "title": "Title", "advanced": "Advanced", "advanced_text": "Advanced connectivity settings for your device. You can only choose one option.", "ttn_info_text": "The openSenseMap offers an integration with <0>TheThingsNetwork. Documentation for the parameters is provided on <1>GitHub", + "MQTT Configuration": "MQTT Configuration", + "mqtt_config_text": "Configure your MQTT settings for data streaming", + "Enable MQTT": "Enable MQTT", + "Enable TTN": "Enable TTN", + "TTN Configuration": "TTN Configuration", + "ttn_config_text": "Configure your TTN (The Things Network) settings", "ttn_app_id_info": "The application ID recieved from TTN", "ttn_dev_id_info": "The device ID recieved from TTN", "ttn_decode_profile_info": "A decoding profile matching the payload format. For details and configuration see <0>here.", @@ -48,10 +78,13 @@ "mqtt_connect_options_info": "A json encoded string with options to supply to the MQTT client", "location": "Location", + "location_info_text": "Select the device's location by clicking on the map or entering latitude and longitude coordinates manually. Drag the marker on the map to adjust the location if needed.", "location_text": "Click on the map to choose a location for your device. You can also enter your coordinates manually or use the geosearch.", "search_placeholder": "Search", "latitude": "Latitude", "longitude": "Longitude", + "enter latitude": "Enter latitude (-90 to 90)", + "enter longitude": "Enter longitude (-180 to 180)", "height": "Height", "height_info_text": "Height above sea level of your selected location in meters. If you have set up your device above ground level (e.g. high building), you should add this to the derived height. The height is espacially important if you have connected a sensor that meassures air pressure to make this meassurements compareable.",