DEV Community

Roberto Luna
Roberto Luna

Posted on

Replacing a Static Sidebar with a Dynamic Top Navigation + Mega Menu in VibeCoding CRM

Replacing a Static Sidebar with a Dynamic Top Navigation + Mega Menu in VibeCoding CRM


TL;DR:

We swapped the legacy sidebar for a responsive top nav with a mega‑menu in the VibeCoding CRM. The new structure pulls navigation data from a single source, tracks user clicks to surface a “Más usados” section, and cleans up duplicated items. This change improves UX, reduces bundle size, and aligns the UI with modern design patterns.

The Problem

The original CRM layout used a left‑hand sidebar (CrmShell.tsx) that was hard‑coded, duplicated across pages, and didn’t adapt to mobile. It also lacked any dynamic ordering of frequently used items. The UI felt stale, and we were shipping the same navigation code twice (once in CrmShell.tsx and again in CrmShellCondos.tsx). The symptom was a sluggish menu that didn’t reflect real user behavior.

Error messages? None. The issue was purely UX: the sidebar was a static list, and users couldn’t easily discover the most relevant sections.

What I Tried First

I started by adding a simple toggle to collapse the sidebar into a hamburger menu. This required minimal changes to the existing component:

// apps/web/src/app/_components/CrmShell.tsx
const SIDEBAR_W = 2; // kept for backward compatibility
Enter fullscreen mode Exit fullscreen mode

But the toggle broke the layout on mobile, and we still had duplicated navigation data. I then experimented with a separate component for the mega‑menu, but it was still tightly coupled to the sidebar logic.

The first attempt failed because it didn’t remove the underlying duplication or introduce a data‑driven navigation source. The menu still looked like a copy‑paste of the sidebar.

The Implementation

1. Removing the Sidebar and Introducing a Top Nav

We removed the SIDEBAR_W constant and the entire sidebar rendering logic. Instead, we added a <TopNav> component that renders a horizontal list of groups and items. The new component pulls its data from a single NAV_* definition.

// apps/web/src/app/_components/CrmShell.tsx
// Removed: const SIDEBAR_W = 2
// New top navigation
export function CrmShell({ children, context = "rentas" }: { children: React.ReactNode; context?: string }) {
  // ... existing imports and helper functions

  // Navigation groups
  const NAV_GROUPS: NavGroup[] = [
    { group: "Ventas", icon: "💰", items: NAV_VENTAS_GROUPS },
    { group: "Rentas", icon: "🏠", items: NAV_RENTAS_GROUPS },
    // Condominios group is now in CrmShellCondos.tsx
  ];

  // Render top nav
  return (
    <div className="crm-shell">
      <nav className="top-nav">
        {NAV_GROUPS.map((g) => (
          <MegaMenu key={g.group} group={g} />
        ))}
      </nav>
      <main>{children}</main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The MegaMenu component receives a NavGroup and renders a dropdown with its items. This keeps the UI logic separate from data.

2. Consolidating Navigation Data

Both CrmShell.tsx and CrmShellCondos.tsx now reference the same NAV_* arrays. The NAV_RENTAS_MAIN and NAV_RENTAS_GROUPS definitions were moved into a shared file (nav.ts), eliminating duplication.

// apps/web/src/app/_components/nav.ts
export const NAV_RENTAS_MAIN: NavItem[] = [
  { href: "/dashboard/hoy", label: "Hoy", icon: "" },
  { href: "/dashboard/mes", label: "Mes", icon: "📆" },
  // ...
];

export const NAV_RENTAS_GROUPS: NavItem[] = [
  // ... same items but grouped
];
Enter fullscreen mode Exit fullscreen mode

3. Dynamic “Más usados” Section

We added a click counter that tracks how often each menu item is selected. The counter is persisted in localStorage and re‑used to order the “Más usados” group.

// apps/web/src/app/_components/CrmShell.tsx
const [clickCounts, setClickCounts] = useState<{ [key: string]: number }>(() => {
  const stored = localStorage.getItem("navClicks");
  return stored ? JSON.parse(stored) : {};
});

function handleItemClick(item: NavItem) {
  const key = item.href;
  const newCounts = { ...clickCounts, [key]: (clickCounts[key] || 0) + 1 };
  setClickCounts(newCounts);
  localStorage.setItem("navClicks", JSON.stringify(newCounts));
  router.push(item.href);
}
Enter fullscreen mode Exit fullscreen mode

The “Más usados” group is now generated at runtime:

// apps/web/src/app/_components/CrmShell.tsx
const mostUsed = Object.entries(clickCounts)
  .sort(([, a], [, b]) => b - a)
  .slice(0, 5)
  .map(([href]) => NAV_RENTAS_MAIN.find((i) => i.href === href))
  .filter(Boolean);

const NAV_GROUPS = [
  { group: "Más usados", icon: "", items: mostUsed },
  // other groups...
];
Enter fullscreen mode Exit fullscreen mode

4. CSS Enhancements

The sidebar used a hover‑only star icon for favorites. We moved that style to the top nav and made it visible on hover or if the item is marked as a favorite.

/* apps/web/src/app/typography.css */
.sidebar .star {
  opacity: 0;
  transition: opacity 0.2s;
}
.sidebar:hover .star,
.sidebar .favorite {
  opacity: 1;
}
Enter fullscreen mode Exit fullscreen mode

We kept the CSS minimal and scoped to the new component class names.

5. Removing Duplicates and Hierarchy Cleanup

The refactor commit 732a028b removed duplicated items from the NAV_MAIN array and established a clear parent‑child hierarchy for the mega menu.

// apps/web/src/app/_components/CrmShell.tsx
const NAV_MAIN: NavItem[] = [
  { href: "/dashboard/hoy", label: "Hoy", icon: "" },
  { href: "/dashboard/mes", label: "Mes", icon: "📆" },
  // Removed duplicates like "/dashboard/hoy" appearing twice
];
Enter fullscreen mode Exit fullscreen mode

The canSee helper now checks user permissions once per group instead of per item, reducing rendering overhead.

Key Takeaway

Centralize navigation data and render UI from that single source.

By moving all nav definitions into a shared module and feeding them into a top‑nav component, we eliminated duplication, made the menu data‑driven, and opened the door for dynamic features like click‑counting. This pattern scales: you can add new groups or items without touching the UI logic, and the UI will automatically adapt.

What's Next

  1. Persist click counts on the server – Move the navClicks state to a lightweight GraphQL mutation so that usage data is shared across devices.
  2. Add a dark mode toggle – Extend the top nav to include a theme switch that updates CSS variables.
  3. Accessibility audit – Ensure the mega‑menu is fully keyboard‑navigable and screen‑reader friendly.

Tags: #vibecoding #buildinpublic #react #typescript #nextjs #frontend #ui #engineering

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México


Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-07-31

#playadev #buildinpublic

Top comments (0)