mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
added loading state to toolbar
This commit is contained in:
parent
e0f1859f58
commit
0c080595a4
4 changed files with 94 additions and 14 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
ChartBarIcon,
|
||||
HomeModernIcon,
|
||||
BuildingOffice2Icon,
|
||||
ArrowPathIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
NavigationMenu,
|
||||
|
|
@ -16,7 +17,7 @@ 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";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -29,6 +30,16 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [loadingHref, setLoadingHref] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleNav = (href: string) => {
|
||||
if (pathname === href) return;
|
||||
setLoadingHref(href);
|
||||
startTransition(() => {
|
||||
router.push(href);
|
||||
});
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
|
|
@ -67,11 +78,12 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
<NavigationMenuList className="flex-wrap">
|
||||
{navItems.map(({ label, icon: Icon, href, match }) => {
|
||||
const isActive = match(pathname);
|
||||
const isLoading = loadingHref === href && isPending;
|
||||
|
||||
return (
|
||||
<NavigationMenuItem key={label}>
|
||||
<button
|
||||
onClick={() => router.push(href)}
|
||||
onClick={() => handleNav(href)}
|
||||
className={cn(
|
||||
"relative flex items-center rounded-md text-xs font-medium p-[3px]",
|
||||
isActive &&
|
||||
|
|
@ -86,7 +98,11 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
: "bg-gray-50 text-gray-800 hover:bg-midblue hover:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
{isLoading ? (
|
||||
<ArrowPathIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{label}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -101,11 +117,12 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
/>
|
||||
{SettingsItems.map(({ label, icon: Icon, href, match }) => {
|
||||
const isActive = match(pathname);
|
||||
const isLoading = loadingHref === href && isPending;
|
||||
|
||||
return (
|
||||
<NavigationMenuItem key={label}>
|
||||
<button
|
||||
onClick={() => router.push(href)}
|
||||
onClick={() => handleNav(href)}
|
||||
className={cn(
|
||||
"relative flex items-center rounded-md text-xs font-medium p-[3px]",
|
||||
isActive &&
|
||||
|
|
@ -120,7 +137,11 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
: "bg-gray-50 text-gray-800 hover:bg-midblue hover:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
{isLoading ? (
|
||||
<ArrowPathIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{label}
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -159,13 +159,16 @@ function EpcDropdown({
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
function handleScroll() {
|
||||
function handleScroll(e: Event) {
|
||||
if (dropdownRef.current && dropdownRef.current.contains(e.target as Node)) return;
|
||||
setOpen(false);
|
||||
}
|
||||
if (open) {
|
||||
|
|
@ -231,7 +234,7 @@ function EpcDropdown({
|
|||
</button>
|
||||
|
||||
{open && (
|
||||
<div style={dropdownStyle} className="rounded-md border border-gray-200 bg-white shadow-lg py-1">
|
||||
<div ref={dropdownRef} style={dropdownStyle} className="rounded-md border border-gray-200 bg-white shadow-lg py-1">
|
||||
{EPC_LETTERS.map((l) => {
|
||||
const isSelected = selected.includes(l);
|
||||
return (
|
||||
|
|
@ -283,6 +286,7 @@ function EnumMultiDropdown({
|
|||
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleOutside(e: MouseEvent) {
|
||||
|
|
@ -290,7 +294,8 @@ function EnumMultiDropdown({
|
|||
setOpen(false);
|
||||
}
|
||||
}
|
||||
function handleScroll() {
|
||||
function handleScroll(e: Event) {
|
||||
if (dropdownRef.current && dropdownRef.current.contains(e.target as Node)) return;
|
||||
setOpen(false);
|
||||
}
|
||||
if (open) {
|
||||
|
|
@ -346,7 +351,7 @@ function EnumMultiDropdown({
|
|||
</button>
|
||||
|
||||
{open && (
|
||||
<div style={dropdownStyle} className="rounded-md border border-gray-200 bg-white shadow-lg py-1">
|
||||
<div ref={dropdownRef} style={dropdownStyle} className="rounded-md border border-gray-200 bg-white shadow-lg py-1">
|
||||
{options.map((opt) => {
|
||||
const isSelected = selectedLabels.includes(opt.label);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
Updater,
|
||||
PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -510,7 +511,7 @@ export default function PropertyTable({
|
|||
<span className="text-xs text-slate-500">
|
||||
Showing{" "}
|
||||
<span className="font-bold text-primary">
|
||||
{filteredTotal.toLocaleString()}
|
||||
{tableData.length.toLocaleString()}
|
||||
</span>{" "}
|
||||
of{" "}
|
||||
<span className="font-bold text-primary">
|
||||
|
|
@ -590,17 +591,16 @@ export default function PropertyTable({
|
|||
|
||||
{/* Export */}
|
||||
{filteredTotal > EXPORT_LIMIT ? (
|
||||
<div
|
||||
title={`Export is available for up to ${EXPORT_LIMIT.toLocaleString()} properties. Refine your filters to enable it.`}
|
||||
>
|
||||
<Tooltip content={`Export is limited to ${EXPORT_LIMIT.toLocaleString()} properties. Refine your filters to enable it.`}>
|
||||
<button
|
||||
disabled
|
||||
className="flex items-center gap-1.5 h-8 px-3 rounded-lg border border-slate-200 bg-slate-100 text-xs font-semibold text-slate-400 cursor-not-allowed opacity-60"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-3.5 w-3.5" />
|
||||
Export
|
||||
<span className="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full bg-amber-400 text-white text-[9px] font-black leading-none">!</span>
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => exportToCsv(tableData)}
|
||||
|
|
|
|||
54
src/app/portfolio/[slug]/components/Tooltip.tsx
Normal file
54
src/app/portfolio/[slug]/components/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface TooltipProps {
|
||||
content: React.ReactNode;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function Tooltip({ content, children }: TooltipProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [style, setStyle] = useState<React.CSSProperties>({});
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !triggerRef.current) return;
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
setStyle({
|
||||
position: "fixed",
|
||||
top: rect.top - 8,
|
||||
left: rect.left + rect.width / 2,
|
||||
transform: "translate(-50%, -100%)",
|
||||
zIndex: 9999,
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
className="inline-flex"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{visible &&
|
||||
createPortal(
|
||||
<div style={style} className="pointer-events-none">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 max-w-[220px] text-center leading-snug shadow-lg">
|
||||
{content}
|
||||
</div>
|
||||
{/* Arrow */}
|
||||
<div className="flex justify-center">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-gray-900" />
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue