DEV Community

Roberto Luna
Roberto Luna

Posted on

Mobile‑First Compact Dashboard & WhatsApp “Finish” Bot in a Next.js 13 App

Mobile‑First Compact Dashboard & WhatsApp “Finish” Bot in a Next.js 13 App

TL;DR: I refactored the ticket‑panel UI to a mobile‑first layout with a bottom TabBar, collapsible filters, and responsive CSS. I also added a “Finish” button to the WhatsApp bot flow. The changes live in app/*, components/TabBar.tsx, and lib/wa‑bot.ts.


The Problem

The original albaida‑tickets panel was built for desktop browsers. When the same view was opened on a phone:

  • The sidebar pushed the main content off‑screen.
  • Filter controls were always expanded, consuming most of the vertical space.
  • There was no persistent navigation for the most common actions (My tickets, New ticket, Settings).

On the bot side, users could navigate the menu but never had a clear way to signal that they were done filing a complaint. The flow ended abruptly after the last message, leaving the conversation hanging.

Both issues manifested as UI/UX dead‑ends, not as runtime errors, but they broke the core promise of the app: “manage tickets from anywhere”.


What I Tried First

1. Pure CSS Media Queries

I added a few @media (max-width: 640px) rules to shrink the sidebar and hide some elements. The layout technically fit on the screen, but:

  • The hidden sidebar still rendered on the server, causing a flash of unstyled content (FOUC).
  • The filter panel remained a static block; users could not collapse it, so the page still felt cramped.

2. Conditional Rendering in app/panel/page.tsx

I wrapped the sidebar in a if (isMobile) return null check using useEffect + window.innerWidth. This broke the server‑component contract because app/panel/page.tsx is a server component; accessing window caused a ReferenceError: window is not defined during SSR.

Both attempts failed to give a clean, maintainable solution.


The Implementation

1. Global CSS – app/globals.css

I expanded the CSS file by 116 lines to introduce a mobile‑first utility set and a new bottom navigation bar.

/* globals.css – added mobile utilities */
@media (max-width: 640px) {
  /* Collapse the side panel */
  .sidebar { display: none; }

  /* Make the main container full‑width */
  .main { margin-left: 0; padding: 0 1rem; }

  /* Bottom tab bar – fixed height */
  .tab-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 56px;
    background: var(--bg-primary);
    display: flex;
    justify-content: space-around;
    align-items: center;
    border-top: 1px solid var(--border);
    z-index: 1000;
  }

  /* Card layout – single column */
  .ticket-card {
    width: 100%;
    margin-bottom: 1rem;
  }

  /* Collapsible filter header */
  details.filter {
    margin-bottom: 0.5rem;
  }
  details.filter > summary {
    cursor: pointer;
    list-style: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • The mobile utilities are scoped under a single @media block, keeping the desktop styles untouched.
  • The .tab-bar class will be used by the new TabBar component.
  • Collapsible filters use native <details>/<summary> elements, eliminating the need for extra JS.

2. Viewport Configuration – app/layout.tsx

A single line was added to the viewport export:

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover", // <- new line
  themeColor: [{ media: "(prefers-color-scheme: dark)", color: "#000000" }],
};
Enter fullscreen mode Exit fullscreen mode

viewportFit: "cover" tells iOS Safari to render the page edge‑to‑edge, preventing the default “letterbox” on iPhone X‑class devices.

3. Panel Layout – app/panel/layout.tsx

I imported the new TabBar component (the import line is the only change in this file):

import { NavLink } from "@/components/NavLink";
import TabBar from "@/components/TabBar"; // <- new import
Enter fullscreen mode Exit fullscreen mode

The layout now renders TabBar only on mobile:

export default function PanelLayout({ children }: { children: ReactNode }) {
  const isMobile = typeof window !== "undefined" && window.innerWidth <= 640;
  return (
    <>
      {children}
      {isMobile && <TabBar />}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Because PanelLayout runs on the client (via the use client directive in TabBar), the window check is safe.

4. Panel Page – app/panel/page.tsx

The diff added 127 lines and removed 93, essentially refactoring the UI to use the new CSS classes and <details> for filters.

Before (simplified)

<div className="filters">
  <select>…</select>
  <input type="date" />
</div>
<div className="cards">{tickets.map(renderCard)}</div>
Enter fullscreen mode Exit fullscreen mode

After

{/* Mobile‑first filter block */}
<details className="filter" open>
  <summary className="font-medium">Filters</summary>
  <div className="mt-2 space-y-2">
    <select name="status" className="w-full rounded border">
      {/* options */}
    </select>
    <input type="date" className="w-full rounded border" />
  </div>
</details>

{/* Ticket cards – now responsive */}
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
  {tickets.map((t) => (
    <article key={t.id} className="ticket-card p-4 rounded shadow">
      {/* card content */}
    </article>
  ))}
</section>
Enter fullscreen mode Exit fullscreen mode
  • The <details> element gives us a native collapsible UI without extra state.
  • The grid layout automatically collapses to a single column on mobile, thanks to the CSS utilities already present.
  • All class names (ticket-card, filter) map directly to the new rules in globals.css.

5. Bottom Navigation – components/TabBar.tsx

A brand‑new client component (62 lines) that renders three navigation links:


tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";

const tabs = [
  { href: "/panel", label: "Tickets", icon: "📋" },
  { href: "/panel/new", label: "New", icon: "➕" },
  { href: "/settings", label: "Settings", icon: "⚙️" },
];

export default function

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/albaida-tickets` · 2026-10-24*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)