mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Refactored portfolio toolbar to allow straight down dropdown
This commit is contained in:
parent
23f6516c20
commit
e6f3737355
6 changed files with 269 additions and 121 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
102
src/app/components/portfolio/YourProjectsDropdown.tsx
Normal file
102
src/app/components/portfolio/YourProjectsDropdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
|
||||
/* 🌞 Light Theme (raw HSL values) */
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue