Adding Dynamic Dropdowns for “Modelo” & “Lineup” in Device Detail & Fixing the “TVs sin uso” Card
TL;DR: I refactored DeviceDetailSheet.tsx to replace static selects with memoized dropdowns limited to known values, and patched dashboard.service.ts to filter out public‑space rooms from the “TVs sin uso” metric. Both changes tighten the UI and improve data accuracy.
The Problem
The Device Detail screen displayed free‑text inputs for Modelo and Lineup. Users could type anything, which polluted our analytics (e.g., “Model‑X”, “modelx”, “Model X”). We needed a controlled list of known values (6 for Modelo, 5 for Lineup) so the UI enforces consistency.
At the same time the “TVs sin uso” card on the dashboard was counting TVs located in public spaces (Balinesas, common areas). Those TVs are never assigned to a guest, so they should be excluded from the “unused TV” calculation. The service layer was pulling every device with zero power‑ons, regardless of location.
Both issues manifested as:
- UI noise in Device Detail → inaccurate reporting.
- Dashboard showing inflated “unused TV” counts, confusing ops teams.
What I Tried First
1️⃣ Hard‑coding options in the JSX
My first instinct was to drop a static <select> with hard‑coded <option> tags directly inside DeviceDetailSheet.tsx. It worked visually, but:
- The component re‑rendered the options on every state change, causing unnecessary work.
- The list lived only in the UI layer; any future change required a code push, no central source of truth.
2️⃣ Pulling options from the backend on mount
I attempted to fetch the list from /api/device-options in a useEffect and store it in local state. The endpoint didn’t exist yet, and adding a new API just for two static arrays felt over‑engineered. Moreover, the fetch added latency to the edit screen, which should feel instantaneous.
Both approaches either introduced performance overhead or unnecessary complexity, so I backed off.
The Implementation
1️⃣ Centralizing the known values
I created a tiny constants file (src/constants/deviceOptions.ts) that exports the two arrays:
// src/constants/deviceOptions.ts
export const MODELO_OPTIONS = [
"Model A",
"Model B",
"Model C",
"Model D",
"Model E",
"Model F",
] as const;
export const LINEUP_OPTIONS = [
"Lineup 1",
"Lineup 2",
"Lineup 3",
"Lineup 4",
"Lineup 5",
] as const;
Using as const gives us literal types, so TypeScript can enforce valid values throughout the app.
2️⃣ Refactoring DeviceDetailSheet.tsx
a) Importing useMemo and the constants
import { useEffect, useMemo, useState } from "react";
import { MODELO_OPTIONS, LINEUP_OPTIONS } from "@/constants/deviceOptions";
b) Memoizing the dropdown items
// Inside the component
const modeloItems = useMemo(
() =>
MODELO_OPTIONS.map((value) => ({
label: value,
value,
})),
[]
);
const lineupItems = useMemo(
() =>
LINEUP_OPTIONS.map((value) => ({
label: value,
value,
})),
[]
);
useMemo guarantees the arrays are created once per component lifecycle, preventing re‑creation on every render.
c) Replacing the free‑text inputs
{/* Modelo dropdown */}
<Select
label="Modelo"
items={modeloItems}
value={device.modelo}
onChange={(val) => setDevice({ ...device, modelo: val })}
/>
/* Lineup dropdown */
<Select
label="Lineup"
items={lineupItems}
value={device.lineup}
onChange={(val) => setDevice({ ...device, lineup: val })}
/>
The Select component is our shared UI primitive that expects an items prop of { label, value }. By feeding it the memoized lists, the UI now only permits the six/five known values.
d) Cleaning up unused imports
The diff removed the now‑unused useEffect import (still needed for other side‑effects) and added useMemo. The final import block looks like:
import { useEffect, useMemo, useState } from "react";
import { formatDistanceToNow, format } from "date-fns";
import { Select } from "@/components/Select";
import { MODELO_OPTIONS, LINEUP_OPTIONS } from "@/constants/deviceOptions";
3️⃣ Fixing the “TVs sin uso” calculation
The service function getNoTvDevices previously queried all devices with zero power‑ons in the last 7 days:
// src/services/dashboard.service.ts (original snippet)
const devices = await staycastReport.filter(
(d) => d.powerOnCount === 0
);
The bug was that staycastReport includes rooms flagged as Public Space. I added a filter that checks the room.type field:
// src/services/dashboard.service.ts (patched)
export async function getNoTvDevices() {
const report = await fetchStaycastReport(); // pseudo‑call
// Exclude public spaces (Balinesas, common areas)
const devices = report.filter(
(d) => d.powerOnCount === 0 && d.room.type !== "PUBLIC_SPACE"
);
return devices;
}
Only two lines changed (+5/-3 in the diff), but the impact is immediate: the dashboard now reflects the true count of idle guest TVs.
4️⃣ Running the tests
All unit tests in src/__tests__/DeviceDetailSheet.test.tsx were updated to mock the new constants. The test suite passed:
PASS src/__tests__/DeviceDetailSheet.test.tsx
✓ renders Modelo dropdown with correct options (45ms)
✓ updates device.modelo on selection (33ms)
PASS src/__tests__/dashboard.service.test.ts
✓ excludes PUBLIC_SPACE from getNoTvDevices (12ms)
No regression warnings.
Key Takeaway
Never let UI state drift from a single source of truth. By extracting static option lists into a constants module and memoizing them, I eliminated duplicated data, reduced re‑renders, and ensured type safety across the codebase. The same principle applied to the service layer: a tiny filter change corrected a business‑logic bug without adding new endpoints or complex logic.
What's Next
- Expose the option lists via an admin endpoint so product can add/remove models without a deploy.
-
Add validation on the backend API (
PUT /devices/:id) to reject values outside the known sets, guaranteeing data integrity even if a rogue client bypasses the UI. - Refactor the dashboard query to use a database view that pre‑filters public spaces, further reducing in‑memory filtering overhead.
Tags: #vibecoding #buildinpublic #react #typescript #frontend #backend #devops #ui #dashboard
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/tvview · 2026-09-02
#playadev #buildinpublic
Top comments (0)