Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 106 additions & 49 deletions src/app/map/[id]/components/inspector/InspectorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { ArrowLeftIcon, MapPinIcon, TableIcon, XIcon } from "lucide-react";
import {
ArrowLeftIcon,
MapPinIcon,
SettingsIcon,
TableIcon,
XIcon,
} from "lucide-react";

import { useInspector } from "@/app/map/[id]/hooks/useInspector";
import { useTable } from "@/app/map/[id]/hooks/useTable";
Expand All @@ -9,10 +15,15 @@ import { Button } from "@/shadcn/ui/button";
import { cn } from "@/shadcn/utils";
import { LayerType } from "@/types";
import { useMapRef } from "../../hooks/useMapCore";
import LayerTypeIcon from "../LayerTypeIcon";
import BoundaryMarkersList from "./BoundaryMarkersList";
import PropertiesList from "./PropertiesList";
import TurfMarkersList from "./TurfMarkersList";
import {
UnderlineTabs,
UnderlineTabsContent,
UnderlineTabsList,
UnderlineTabsTrigger,
} from "./UnderlineTabs";

export default function InspectorPanel() {
const {
Expand All @@ -22,6 +33,7 @@ export default function InspectorPanel() {
selectedTurf,
focusedRecord,
setFocusedRecord,
selectedRecords,
} = useInspector();
const mapRef = useMapRef();
const { setSelectedDataSourceId, selectedDataSourceId } = useTable();
Expand Down Expand Up @@ -49,6 +61,8 @@ export default function InspectorPanel() {
(selectedTurf && type !== LayerType.Turf) ||
(selectedBoundary && type !== LayerType.Boundary);

const markerCount = selectedRecords?.length || 0;

const onCloseDetailsView = () => {
setFocusedRecord(null);
};
Expand All @@ -65,15 +79,14 @@ export default function InspectorPanel() {
<div
id="inspector-panel"
className={cn(
"absolute top-0 bottom-0 right-4 / flex flex-col gap-6 w-60 py-5 h-fit max-h-full",
"absolute top-0 bottom-0 right-4 / flex flex-col gap-6 py-5 h-fit max-h-full",
tableOpen ? "bottom-0" : "bottom-24", // to avoid clash with bug report button
)}
style={{ minWidth: "250px" }}
>
<div className="relative z-100 w-full max-h-full overflow-auto / flex flex-col / rounded shadow-lg bg-white / text-sm font-sans">
<div className="flex justify-between items-start gap-4 p-4">
<div className="flex justify-between items-center gap-4 p-3">
<h1 className="grow flex gap-2 / text-sm font-semibold">
<LayerTypeIcon type={inspectorContent?.type} className="mt-1" />

{inspectorContent?.name as string}
</h1>
<button
Expand Down Expand Up @@ -106,54 +119,98 @@ export default function InspectorPanel() {
</div>
)}

<div className="grow overflow-auto flex flex-col gap-4 [&:not(:empty)]:border-t [&:not(:empty)]:p-4">
{dataSource && (
<div className="bg-muted py-1 px-2 rounded">
<h3 className="mb-1 / text-muted-foreground text-xs uppercase font-mono">
Data source
</h3>
<div className="flex items-center gap-2">
<div className="shrink-0">
<DataSourceIcon type={dataSource.config?.type as string} />
<UnderlineTabs
defaultValue="data"
className="flex flex-col overflow-hidden"
>
<UnderlineTabsList className="w-full flex gap-6 border-t px-3">
<UnderlineTabsTrigger value="data">Data</UnderlineTabsTrigger>
<UnderlineTabsTrigger value="markers">
Markers {markerCount > 0 ? markerCount : ""}
</UnderlineTabsTrigger>
<UnderlineTabsTrigger value="notes">Notes 0</UnderlineTabsTrigger>
<UnderlineTabsTrigger value="config" className="px-2">
<SettingsIcon size={16} />
</UnderlineTabsTrigger>
</UnderlineTabsList>

<UnderlineTabsContent value="data" className="grow overflow-auto p-3">
<div className="flex flex-col gap-4">
{dataSource && (
<div className="bg-muted py-1 px-2 rounded">
<h3 className="mb-1 / text-muted-foreground text-xs uppercase font-mono">
Data source
</h3>
<div className="flex items-center gap-2">
<div className="shrink-0">
<DataSourceIcon
type={dataSource.config?.type as string}
/>
</div>

<p className="truncate">{dataSource.name}</p>
</div>
</div>
)}

<p className="truncate">{dataSource.name}</p>
</div>
</div>
)}

{recordLoading ? (
<span>Loading...</span>
) : (
<PropertiesList
properties={{ ...properties, ...recordData?.json }}
/>
)}

{type === LayerType.Turf && <TurfMarkersList />}

{type === LayerType.Boundary && <BoundaryMarkersList />}

{(isDetailsView || dataSource) && (
<div className="flex flex-col gap-3 border-t pt-4">
{isDetailsView && focusedRecord?.geocodePoint && (
<Button onClick={() => flyToMarker()}>
<MapPinIcon />
View on map
</Button>
{recordLoading ? (
<span>Loading...</span>
) : (
<PropertiesList
properties={{ ...properties, ...recordData?.json }}
/>
)}
{dataSource && (
<Button
variant="secondary"
onClick={() => setSelectedDataSourceId(dataSource.id)}
>
<TableIcon />
View in table
</Button>

{(isDetailsView || dataSource) && (
<div className="flex flex-col gap-3 border-t pt-4">
{isDetailsView && focusedRecord?.geocodePoint && (
<Button onClick={() => flyToMarker()}>
<MapPinIcon />
View on map
</Button>
)}
{dataSource && (
<Button
variant="secondary"
onClick={() => setSelectedDataSourceId(dataSource.id)}
>
<TableIcon />
View in table
</Button>
)}
</div>
)}
</div>
)}
</div>
</UnderlineTabsContent>

<UnderlineTabsContent
value="markers"
className="grow overflow-auto p-3"
>
<div className="flex flex-col gap-4">
{type === LayerType.Turf && <TurfMarkersList />}
{type === LayerType.Boundary && <BoundaryMarkersList />}
</div>
</UnderlineTabsContent>

<UnderlineTabsContent
value="notes"
className="grow overflow-auto p-3"
>
<div className="flex flex-col gap-4">
<p className="text-muted-foreground">No notes yet</p>
</div>
</UnderlineTabsContent>

<UnderlineTabsContent
value="config"
className="grow overflow-auto p-3"
>
<div className="flex flex-col gap-4">
<p className="text-muted-foreground">Configuration options</p>
</div>
</UnderlineTabsContent>
</UnderlineTabs>
</div>
</div>
);
Expand Down
133 changes: 133 additions & 0 deletions src/app/map/[id]/components/inspector/UnderlineTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";

import { cn } from "@/shadcn/utils";

function UnderlineTabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root className={cn("flex flex-col", className)} {...props} />
);
}

function UnderlineTabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
const [underlineStyle, setUnderlineStyle] = React.useState({
left: 0,
width: 0,
});
const listRef = React.useRef<HTMLDivElement>(null);

const updateUnderline = React.useCallback(() => {
if (!listRef.current) return;

const activeButton = listRef.current.querySelector(
'[data-state="active"]',
) as HTMLButtonElement;
if (!activeButton) return;

const listRect = listRef.current.getBoundingClientRect();
const buttonRect = activeButton.getBoundingClientRect();

setUnderlineStyle({
left: buttonRect.left - listRect.left,
width: buttonRect.width,
});
}, []);

React.useEffect(() => {
updateUnderline();

// Use MutationObserver to watch for data-state changes
const mutationObserver = new MutationObserver(updateUnderline);
if (listRef.current) {
mutationObserver.observe(listRef.current, {
attributes: true,
attributeFilter: ["data-state"],
subtree: true,
});
}

// Also use ResizeObserver for size changes
const resizeObserver = new ResizeObserver(updateUnderline);
if (listRef.current) {
resizeObserver.observe(listRef.current);
}

window.addEventListener("resize", updateUnderline);

return () => {
mutationObserver.disconnect();
resizeObserver.disconnect();
window.removeEventListener("resize", updateUnderline);
};
}, [updateUnderline]);

return (
<TabsPrimitive.List
ref={listRef}
className={cn(
"relative inline-flex w-full items-center justify-start border-b border-border",
className,
)}
{...props}
>
{props.children}
<div
className="absolute bottom-0 h-[2px] bg-foreground transition-all duration-300 ease-out pointer-events-none"
style={{
left: `${underlineStyle.left}px`,
width: `${underlineStyle.width}px`,
}}
/>
</TabsPrimitive.List>
);
}

function UnderlineTabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
className={cn(
"relative inline-flex w-fit items-center justify-center py-3 h-auto text-sm font-medium text-muted-foreground whitespace-nowrap transition-colors cursor-pointer",
"hover:text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
"data-[state=active]:text-foreground",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
/>
);
}

function UnderlineTabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
className={cn(
"flex-1 outline-none animate-in fade-in duration-300",
className,
)}
{...props}
/>
);
}

export {
UnderlineTabs,
UnderlineTabsList,
UnderlineTabsTrigger,
UnderlineTabsContent,
};
Loading