Skip to content

feat: restore compare functionality #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/components/compare-devices/compare-device-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useNavigate, useNavigation, useSearchParams } from 'react-router'
import { X } from 'lucide-react'

Check warning on line 2 in app/components/compare-devices/compare-device-box.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`lucide-react` import should occur before import of `react-router`
import Spinner from '../spinner'
import { DeviceComparison } from './device-comparison'

export default function CompareDevices({
devicesWithSensors,
}: {
devicesWithSensors: any[]
}) {
const navigate = useNavigate()
const navigation = useNavigation()
const [searchParams] = useSearchParams()

const allSensors = devicesWithSensors.flatMap((device) => device.sensors)

Check warning on line 15 in app/components/compare-devices/compare-device-box.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'allSensors' is assigned a value but never used. Allowed unused vars must match /^ignored/u
// const uniqueSensorTitles = [
// ...new Set(allSensors.map((sensor) => sensor.title)),
// ]

return (
<>
<div className="absolute bottom-6 left-4 right-4 top-14 z-40 flex flex-row px-4 py-2 md:bottom-[30px] md:left-[10px] md:top-auto md:max-h-[calc(100vh-8rem)] md:w-1/3 md:p-0">
<div className="shadow-zinc-800/5 ring-zinc-900/5 relative float-left flex h-full max-h-[calc(100vh-4rem)] w-auto flex-col gap-4 rounded-xl bg-white px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg ring-1 dark:bg-zinc-800 dark:text-zinc-200 dark:opacity-95 dark:ring-white dark:backdrop-blur-sm md:max-h-[calc(100vh-8rem)]">
{navigation.state === 'loading' && (
<div className="bg-white/30 dark:bg-zinc-800/30 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
<Spinner />
</div>
)}
{/* this is the header */}
<div className="flex w-full cursor-move items-end gap-3 py-2">
<X
className="cursor-pointer ml-auto"
onClick={() => {
void navigate({
pathname: '/explore',
search: searchParams.toString(),
})
}}
/>
</div>
<div className="no-scrollbar relative flex-1 overflow-y-scroll">
<div className="no-scrollbar relative flex-1 overflow-y-auto">
<DeviceComparison devices={devicesWithSensors} />
</div>
</div>
</div>
</div>
</>
)
}
30 changes: 30 additions & 0 deletions app/components/compare-devices/device-comparison.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import DeviceInfo from './device-info'
import { SensorComparison } from './sensor-comparison'
import { Separator } from '../ui/separator'

Check warning on line 3 in app/components/compare-devices/device-comparison.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`../ui/separator` import should occur before import of `./device-info`

interface DeviceComparisonProps {
devices: any[]
}

export function DeviceComparison({ devices }: DeviceComparisonProps) {
const allSensors = devices.flatMap((device) => device.sensors)
const uniqueSensorTitles = [
...new Set(allSensors.map((sensor) => sensor.title)),
]

return (
<div className="space-y-4">
<div className="flex items-center gap-4">
{devices.map((device) => (
<DeviceInfo
key={device.id}
device={device}
otherDeviceId={devices.find((d) => d.id !== device.id)?.id}
/>
))}
<Separator orientation="vertical" />
</div>
<SensorComparison devices={devices} sensorTitles={uniqueSensorTitles} />
</div>
)
}
51 changes: 51 additions & 0 deletions app/components/compare-devices/device-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Link } from 'react-router'
import { type Device } from '~/schema'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'

Check warning on line 3 in app/components/compare-devices/device-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`../ui/card` import should occur before import of `~/schema`
import { ExposureBadge } from './exposure-badge'

Check warning on line 4 in app/components/compare-devices/device-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`./exposure-badge` import should occur before import of `~/schema`
import { StatusBadge } from './status-badge'

Check warning on line 5 in app/components/compare-devices/device-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`./status-badge` import should occur before import of `~/schema`
import { Trash } from 'lucide-react'

Check warning on line 6 in app/components/compare-devices/device-info.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`lucide-react` import should occur before import of `react-router`

export default function DeviceInfo({
device,
otherDeviceId,
}: {
device: Device
otherDeviceId: string
}) {
return (
<Card className="w-full">
<CardHeader>
<div className="flex justify-between">
<CardTitle>{device.name}</CardTitle>
<Link to={`/explore/${otherDeviceId}`}>
<Trash className="h-4 w-4" />
</Link>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between">
<span className="font-semibold">Status:</span>
<StatusBadge status={device.status ?? 'inactive'} />
</div>
<div className="flex justify-between">
<span className="font-semibold">Exposure:</span>
<ExposureBadge exposure={device.exposure ?? 'unknown'} />
</div>
<div className="flex justify-between">
<span className="font-semibold">Created:</span>
<span>{new Date(device.createdAt).toLocaleDateString()}</span>
</div>
<div className="flex justify-between">
<span className="font-semibold">Updated:</span>
<span>{new Date(device.updatedAt).toLocaleDateString()}</span>
</div>
{device.sensorWikiModel && (
<div className="flex justify-between">
<span className="font-semibold">Model:</span>
<span>{device.sensorWikiModel}</span>
</div>
)}
</CardContent>
</Card>
)
}
14 changes: 14 additions & 0 deletions app/components/compare-devices/exposure-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Badge } from "../ui/badge"

export type DeviceExposureEnum = "indoor" | "outdoor" | "mobile" | "unknown"

export function ExposureBadge({ exposure }: { exposure: DeviceExposureEnum }) {
const colorMap: Record<string, string> = {
indoor: "bg-blue-500",
outdoor: "bg-green-500",
mobile: "bg-purple-500",
unknown: "bg-gray-500",
}

return <Badge className={`${colorMap[exposure]} text-white`}>{exposure}</Badge>
}
142 changes: 142 additions & 0 deletions app/components/compare-devices/sensor-comparison.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
Link,
useLocation,

Check warning on line 3 in app/components/compare-devices/sensor-comparison.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'useLocation' is defined but never used. Allowed unused vars must match /^ignored/u
useMatches,
useParams,
useSearchParams,
} from 'react-router'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../ui/table'
import { toast } from '../ui/use-toast'
import { Minus } from 'lucide-react'

Check warning on line 17 in app/components/compare-devices/sensor-comparison.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`lucide-react` import should occur before import of `react-router`

interface SensorComparisonProps {
devices: any[]
sensorTitles: string[]
}

export function SensorComparison({
devices,
sensorTitles,
}: SensorComparisonProps) {
const matches = useMatches()
const [searchParams] = useSearchParams()
const { deviceId, deviceId2 } = useParams()

const sensorIds = new Set()

const createSensorLink = (sensorIdToBeSelected: string) => {
const lastSegment = matches[matches.length - 1]?.params?.['*']
if (lastSegment) {
const secondLastSegment = matches[matches.length - 2]?.params?.sensorId
sensorIds.add(secondLastSegment)
sensorIds.add(lastSegment)
} else {
const lastSegment = matches[matches.length - 1]?.params?.sensorId
if (lastSegment) {
sensorIds.add(lastSegment)
}
}

// If sensorIdToBeSelected is second selected sensor
if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 2) {
const clonedSet = new Set(sensorIds)
clonedSet.delete(sensorIdToBeSelected)
return `/explore/${deviceId}/compare/${deviceId2}/${Array.from(clonedSet).join('/')}?${searchParams.toString()}`
} else if (sensorIds.has(sensorIdToBeSelected) && sensorIds.size === 1) {
return `/explore/${deviceId}/compare/${deviceId2}?${searchParams.toString()}`
} else if (sensorIds.size === 0) {
return `/explore/${deviceId}/compare/${deviceId2}/${sensorIdToBeSelected}?${searchParams.toString()}`
} else if (sensorIds.size === 1) {
return `/explore/${deviceId}/compare/${deviceId2}/${Array.from(sensorIds).join('/')}/${sensorIdToBeSelected}?${searchParams.toString()}`
}

return ''
}

const isSensorActive = (sensorId: string) => {
if (sensorIds.has(sensorId)) {
return 'bg-green-100 dark:bg-dark-green'
}
return 'hover:bg-muted'
}

return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Sensor</TableHead>
{devices.map((device) => (
<TableHead key={device.id}>{device.name}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{sensorTitles.map((title) => (
<TableRow key={title}>
<TableCell className="font-medium">{title}</TableCell>
{devices.map((device) => {
const sensor = device.sensors.find((s: any) => s.title === title)

// If sensor is undefined, return a placeholder cell
if (!sensor) {
return (
<TableCell key={device.id}>
<Minus />
</TableCell>
)
}

const sensorLink = createSensorLink(sensor.id)
if (sensorLink === '') {
return (
<TableCell
key={device.id}
onClick={() =>
toast({
title: "Can't select more than 2 sensors",
description: 'Deselect one sensor to select another',
variant: 'destructive',
})
}
>
<div>
<div>
{sensor.value} {sensor.unit}
</div>
<div className="text-sm text-muted-foreground">
Last updated: {new Date(sensor.time).toLocaleString()}
</div>
</div>
</TableCell>
)
}

return (
<TableCell
key={device.id}
className={isSensorActive(sensor.id)}
>
<Link to={sensorLink}>
<div>
{sensor.value} {sensor.unit}
</div>
<div className="text-sm text-muted-foreground">
Last updated: {new Date(sensor.time).toLocaleString()}
</div>
</Link>
</TableCell>
)
})}
</TableRow>
))}
</TableBody>
</Table>
)
}
15 changes: 15 additions & 0 deletions app/components/compare-devices/status-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Badge } from '@/components/ui/badge'

export type StatusBadgeProps = {
status: 'active' | 'inactive' | 'old'
}

export function StatusBadge({ status }: StatusBadgeProps) {
const colorMap = {
active: 'bg-green-500',
inactive: 'bg-red-500',
old: 'bg-slate-500',
}

return <Badge className={`${colorMap[status]} text-white`}>{status}</Badge>
}
Loading
Loading