DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Dynamic Dropdowns for “Modelo” & “Lineup” in Device Detail & Fixing the “TVs sin uso” Card

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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,
    })),
  []
);
Enter fullscreen mode Exit fullscreen mode

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 })}
/>
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)