From e82208f897c7c90bd6c1e9baf8c6285ef9b7550b Mon Sep 17 00:00:00 2001 From: rex-bot Date: Thu, 21 May 2026 11:51:40 +0000 Subject: [PATCH] CUB-196: add CameraCard component with battery bar, recording indicator, online status --- src/components/CameraCard.tsx | 164 ++++++++++++++++++++++++++++++++++ src/components/index.ts | 1 + 2 files changed, 165 insertions(+) create mode 100644 src/components/CameraCard.tsx create mode 100644 src/components/index.ts diff --git a/src/components/CameraCard.tsx b/src/components/CameraCard.tsx new file mode 100644 index 0000000..bb67ab4 --- /dev/null +++ b/src/components/CameraCard.tsx @@ -0,0 +1,164 @@ +import { Video, Wifi, WifiOff, Signal, Battery, Radio } from 'lucide-react' +import type { CameraStatus } from '../types' + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function formatRelativeTime(iso: string): string { + const now = Date.now() + const then = new Date(iso).getTime() + const diffSec = Math.floor((now - then) / 1000) + + if (diffSec < 10) return 'just now' + if (diffSec < 60) return `${diffSec}s ago` + + const diffMin = Math.floor(diffSec / 60) + if (diffMin < 60) return `${diffMin}m ago` + + const diffHr = Math.floor(diffMin / 60) + if (diffHr < 24) return `${diffHr}h ago` + + const diffDay = Math.floor(diffHr / 24) + return `${diffDay}d ago` +} + +function batteryColor(pct: number | null): { bar: string; text: string } { + if (pct === null) return { bar: 'bg-rig-dark-600', text: 'text-rig-dark-400' } + if (pct >= 50) return { bar: 'bg-rig-success', text: 'text-rig-success' } + if (pct >= 15) return { bar: 'bg-rig-warning', text: 'text-rig-warning' } + return { bar: 'bg-rig-danger', text: 'text-rig-danger' } +} + +function formatTimeLeft(sec: number): string { + const m = Math.floor(sec / 60) + const s = sec % 60 + return `${m}m ${s}s left` +} + +// ── Component ────────────────────────────────────────────────────────────── + +interface CameraCardProps { + camera: CameraStatus +} + +export default function CameraCard({ camera }: CameraCardProps) { + const { + friendly_name, + online, + resolution, + fps, + recording, + mode, + battery_pct, + last_seen, + video_remaining_sec, + } = camera + + const batt = batteryColor(battery_pct) + + return ( +
+ {/* ── Header ── */} +
+
+
+ + {/* Online / Offline badge */} + + {online ? ( + + ) : ( + + )} + {online ? 'Online' : 'Offline'} + +
+ + {/* ── Body ── */} +
+ {/* Resolution + FPS */} +
+ + + {resolution} · {fps} FPS + +
+ + {/* Recording state */} +
+ {recording ? ( + <> + + + + + + REC + + + ) : ( + + IDLE + + )} + {mode} +
+ + {/* Battery */} +
+
+ + + Battery + + + {battery_pct !== null ? `${battery_pct}%` : 'N/A'} + +
+
+
+
+
+
+ + {/* ── Footer ── */} +
+
+ {online ? ( + <> + + Live + + ) : ( + {formatRelativeTime(last_seen)} + )} +
+ + {video_remaining_sec !== null && ( +
+ + {formatTimeLeft(video_remaining_sec)} +
+ )} +
+
+ ) +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..df5de56 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { default as CameraCard } from './CameraCard'