Refactored portfolio toolbar to allow straight down dropdown

This commit is contained in:
Khalim Conn-Kowlessar 2025-11-13 17:14:20 +00:00
parent 23f6516c20
commit e6f3737355
6 changed files with 269 additions and 121 deletions

View file

@ -1,90 +1,111 @@
"use client";
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuTrigger,
} from "@/app/shadcn_components/ui/navigation-menu";
import { useRouter } from "next/navigation";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import {
TableCellsIcon,
DocumentMagnifyingGlassIcon,
ChevronDownIcon,
DocumentPlusIcon,
} from "@heroicons/react/24/outline";
import * as React from "react";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
const ListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<div className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</div>
</a>
</NavigationMenuLink>
</li>
);
});
ListItem.displayName = "ListItem";
interface AddNewProps {
portfolioId: string;
isUploadCsvOpen: boolean;
setIsUploadCsvOpen: Dispatch<SetStateAction<boolean>>;
}
export default function AddNewDropDown({
export default function AddNew({
portfolioId,
isUploadCsvOpen,
setIsUploadCsvOpen,
isRemoteAssessmentOpen,
setIsRemoteAssessmentOpen,
}: {
portfolioId: string;
isUploadCsvOpen: boolean;
setIsUploadCsvOpen: React.Dispatch<React.SetStateAction<boolean>>;
isRemoteAssessmentOpen: boolean;
setIsRemoteAssessmentOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
function handleClickUploadCSV() {
setIsUploadCsvOpen(!isUploadCsvOpen);
}
}: AddNewProps) {
const router = useRouter();
const [loadingRemote, setLoadingRemote] = useState(false);
function handleClickRemoteAssessment() {
function handleRemoteAssessment() {
setLoadingRemote(true);
router.push(`/portfolio/${portfolioId}/remote-assessment`);
}
return (
<NavigationMenuItem>
<NavigationMenuTrigger className="bg-gray-50 text-gray-900">
<Menu as="div" className="relative inline-block text-left">
<MenuButton
className="
inline-flex items-center gap-1 px-4 py-2 rounded-md
bg-gray-50 text-gray-900 hover:bg-midblue hover:text-gray-100
transition-colors text-sm font-medium
"
>
<DocumentPlusIcon className="h-4 w-4 mr-2" />
New Property
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="p-6 md:w-[200px] lg:w-[350px] lg:grid-cols-[.75fr_1fr] cursor-pointer">
<ListItem onClick={handleClickRemoteAssessment}>
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
<DocumentMagnifyingGlassIcon className="h-4 w-4 mr-2" /> Remote
Assessment
</div>
Run a remote assessment for a single unit
</ListItem>
<ListItem onClick={handleClickUploadCSV}>
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
<TableCellsIcon className="h-4 w-4 mr-2" /> File Import
</div>
Upload a excel or csv file, containing multiple units
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<ChevronDownIcon className="h-4 w-4 opacity-70" />
</MenuButton>
<MenuItems
className="
absolute right-0 mt-3 w-72 origin-top-right rounded-md
bg-white shadow-lg ring-1 ring-black/5 focus:outline-none
z-[9999] py-3
"
>
<div className="flex flex-col gap-2 px-3">
{/* Remote Assessment */}
<MenuItem>
{({ active }) => (
<button
onClick={handleRemoteAssessment}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<DocumentMagnifyingGlassIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
Remote Assessment
{loadingRemote && (
<span className="h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin"></span>
)}
</span>
<span className="text-xs text-gray-500 leading-snug">
Run a remote assessment for a single property.
</span>
</div>
</button>
)}
</MenuItem>
{/* CSV Upload */}
<MenuItem>
{({ active }) => (
<button
onClick={() => setIsUploadCsvOpen(!isUploadCsvOpen)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<TableCellsIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
File Import
</span>
<span className="text-xs text-gray-500 leading-snug">
Upload an Excel or CSV file containing multiple units.
</span>
</div>
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Menu>
);
}

View file

@ -2,17 +2,18 @@
import {
Cog6ToothIcon,
BuildingOfficeIcon,
ChartBarIcon,
HomeModernIcon,
RocketLaunchIcon,
BuildingOffice2Icon,
} from "@heroicons/react/24/outline";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuViewport,
} from "@/app/shadcn_components/ui/navigation-menu";
import AddNewDropDown from "./AddNew";
import YourProjectsDropdown from "./YourProjectsDropdown";
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
import { ScenarioSelect } from "@/app/db/schema/recommendations";
import { useState } from "react";
@ -28,12 +29,11 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
const router = useRouter();
const pathname = usePathname();
const [modalIsOpen, setModalIsOpen] = useState(false);
const [isRemoteAssessmentOpen, setIsRemoteAssessmentOpen] = useState(false);
const navItems = [
{
label: "Portfolio",
icon: BuildingOfficeIcon,
icon: BuildingOffice2Icon,
match: (p: string) => p === `/portfolio/${portfolioId}`,
href: `/portfolio/${portfolioId}`,
},
@ -50,13 +50,9 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
p.startsWith(`/portfolio/${portfolioId}/decent-homes`),
href: `/portfolio/${portfolioId}/decent-homes`,
},
{
label: "Your Projects",
icon: RocketLaunchIcon,
match: (p: string) =>
p.startsWith(`/portfolio/${portfolioId}/your-projects`),
href: `/portfolio/${portfolioId}/your-projects/plan`,
},
];
const SettingsItems = [
{
label: "Settings",
icon: Cog6ToothIcon,
@ -66,46 +62,75 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
];
return (
<NavigationMenu>
<NavigationMenuList>
{navItems.map(({ label, icon: Icon, href, match }) => {
const isActive = match(pathname);
<>
<NavigationMenu className="relative">
<NavigationMenuList className="flex-wrap">
{navItems.map(({ label, icon: Icon, href, match }) => {
const isActive = match(pathname);
return (
<NavigationMenuItem key={label} className="mx-1">
<button
onClick={() => router.push(href)}
className={cn(
"relative flex items-center justify-center rounded-md text-sm font-medium transition-all duration-300 p-[3px]",
isActive
? "bg-gradient-to-r from-brandblue via-brandbrown to-brandblue"
: ""
)}
>
<div
return (
<NavigationMenuItem key={label}>
<button
onClick={() => router.push(href)}
className={cn(
"flex h-full w-full items-center justify-center rounded-md px-4 py-2 transition-colors duration-300",
isActive
? "bg-white text-brandblue shadow-sm"
: "bg-gray-50 text-gray-800 hover:text-brandblue hover:bg-midblue hover:text-gray-100"
"relative flex items-center rounded-md text-sm font-medium p-[3px]",
isActive &&
"bg-gradient-to-r from-brandblue via-brandbrown to-brandblue"
)}
>
<Icon className="h-4 w-4 mr-2" />
{label}
</div>
</button>
</NavigationMenuItem>
);
})}
<div
className={cn(
"flex items-center rounded-md px-4 py-2",
isActive
? "bg-white text-brandblue shadow-sm"
: "bg-gray-50 text-gray-800 hover:bg-midblue hover:text-gray-100"
)}
>
<Icon className="h-4 w-4 mr-2" />
{label}
</div>
</button>
</NavigationMenuItem>
);
})}
<YourProjectsDropdown portfolioId={portfolioId} />
<AddNewDropDown
portfolioId={portfolioId}
isUploadCsvOpen={modalIsOpen}
setIsUploadCsvOpen={setModalIsOpen}
/>
{SettingsItems.map(({ label, icon: Icon, href, match }) => {
const isActive = match(pathname);
<AddNewDropDown
portfolioId={portfolioId}
isUploadCsvOpen={modalIsOpen}
setIsUploadCsvOpen={setModalIsOpen}
isRemoteAssessmentOpen={isRemoteAssessmentOpen}
setIsRemoteAssessmentOpen={setIsRemoteAssessmentOpen}
/>
</NavigationMenuList>
return (
<NavigationMenuItem key={label}>
<button
onClick={() => router.push(href)}
className={cn(
"relative flex items-center rounded-md text-sm font-medium p-[3px]",
isActive &&
"bg-gradient-to-r from-brandblue via-brandbrown to-brandblue"
)}
>
<div
className={cn(
"flex items-center rounded-md px-4 py-2",
isActive
? "bg-white text-brandblue shadow-sm"
: "bg-gray-50 text-gray-800 hover:bg-midblue hover:text-gray-100"
)}
>
<Icon className="h-4 w-4 mr-2" />
{label}
</div>
</button>
</NavigationMenuItem>
);
})}
</NavigationMenuList>
<NavigationMenuViewport />
</NavigationMenu>
<UploadCsvModal
isOpen={modalIsOpen}
@ -113,6 +138,6 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
portfolioId={portfolioId}
scenarios={scenarios}
/>
</NavigationMenu>
</>
);
}

View file

@ -0,0 +1,102 @@
"use client";
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
import {
ChevronDownIcon,
ArrowTrendingUpIcon,
CalendarDaysIcon,
RocketLaunchIcon,
} from "@heroicons/react/24/outline";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
export default function YourProjectsDropdown({
portfolioId,
}: {
portfolioId: string;
}) {
const router = useRouter();
function handlePlanClick(portfolioId: string) {
router.push(`/portfolio/${portfolioId}/your-projects/plan`);
}
function handleLiveTrackingClick(portfolioId: string) {
router.push(`/portfolio/${portfolioId}/your-projects/live`);
}
return (
<Menu as="div" className="relative inline-block text-left">
<MenuButton
className="
inline-flex items-center gap-1 px-4 py-2 rounded-md
bg-gray-50 text-gray-900 hover:bg-midblue hover:text-gray-100
transition-colors text-sm font-medium
"
>
<RocketLaunchIcon className="h-4 w-4 mr-2" />
Your Projects
<ChevronDownIcon className="h-4 w-4 opacity-70" />
</MenuButton>
<MenuItems
className="
absolute right-0 mt-3 w-72 origin-top-right rounded-md
bg-white shadow-lg ring-1 ring-black/5 focus:outline-none
z-[9999] py-3
"
>
<div className="flex flex-col gap-2 px-3">
{/* Plans */}
<MenuItem>
{({ active }) => (
<button
onClick={() => handlePlanClick(portfolioId)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<CalendarDaysIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 flex items-center gap-2">
Plans
</span>
<span className="text-xs text-gray-500 leading-snug">
Your project overviews - all metrics and expected
improvements
</span>
</div>
</button>
)}
</MenuItem>
{/* Live Tracking */}
<MenuItem>
{({ active }) => (
<button
onClick={() => handleLiveTrackingClick(portfolioId)}
className={cn(
"w-full p-3 rounded-lg text-left flex gap-3 transition-colors",
active && "bg-gray-100"
)}
>
<ArrowTrendingUpIcon className="h-5 w-5 text-gray-700 mt-[2px]" />
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
Live Tracking
</span>
<span className="text-xs text-gray-500 leading-snug">
See the performance data for your live Domna projects
</span>
</div>
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Menu>
);
}

View file

@ -2,8 +2,6 @@
@tailwind components;
@tailwind utilities;
/* 🌞 Light Theme (raw HSL values) */
:root {
--background: 0 0% 100%;

View file

@ -49,11 +49,11 @@ export default async function RootLayout({
return (
<html lang="en" className={inter.className}>
<body className="min-h-screen flex flex-col">
<body className="min-h-screen">
<Provider>
<ReactQueryProvider>
<Nav userImage={userImage} />
<main className="flex-grow">{children}</main>
<main className="flex flex-col flex-grow">{children}</main>
<Toaster />
<Footer />
</ReactQueryProvider>

View file

@ -1,6 +1,8 @@
import { Toolbar } from "@/app/components/portfolio/Toolbar";
import { getPortfolio, getPortfolioScenarios } from "../utils";
import * as React from "react";
export default async function PortfolioLayout(props: {
children: React.ReactNode;
params: Promise<{ slug: string; propertyId: string }>;
@ -26,7 +28,7 @@ export default async function PortfolioLayout(props: {
</div>
<div className="flex justify-center">
<div className="grid grid-cols-8 w-full max-w-8xl">
<div className="col-span-12 justify-center bg-gray-50 py-2">
<div className="col-span-12 justify-center bg-gray-50 py-2 relative">
<Toolbar portfolioId={portfolioId} scenarios={scenarios} />
</div>
</div>