Skip to content

Commit

Permalink
adds popup to map
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez authored and Andrés González committed Nov 13, 2024
1 parent 9669744 commit 727e68f
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 9 deletions.
7 changes: 7 additions & 0 deletions client/src/app/(overview)/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { MapMouseEvent } from "react-map-gl";

import { atom } from "jotai";

export const projectsUIState = atom<{
filtersOpen: boolean;
}>({
filtersOpen: false,
});

export const popupAtom = atom<{
lngLat: MapMouseEvent["lngLat"];
features: MapMouseEvent["features"];
} | null>(null);
10 changes: 10 additions & 0 deletions client/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ body {
@apply bg-background text-foreground antialiased;
}
}

.mapboxgl-popup-tip {
@apply !border-t-popover !z-10 relative;
}
.mapboxgl-popup-content {
@apply !bg-popover !rounded-md border border-border text-big-stone-50 shadow-md;
}
.mapboxgl-popup-close-button {
@apply hidden;
}
28 changes: 26 additions & 2 deletions client/src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ export default function Map({
};
}, [bounds, isFlying]);

useEffect(() => {
if (!mapRef || !mapboxProps.interactiveLayerIds?.length) return;

const setPointerStyle = () => {
mapRef.getCanvas().style.cursor = "pointer";
};

const removePointerStyle = () => {
mapRef.getCanvas().style.cursor = "";
};

mapboxProps.interactiveLayerIds.forEach((layerId) => {
mapRef.on("mouseenter", layerId, setPointerStyle);
mapRef.on("mouseleave", layerId, removePointerStyle);
});

return () => {
if (!mapRef || !mapboxProps.interactiveLayerIds?.length) return;

mapboxProps.interactiveLayerIds.forEach((layerId) => {
mapRef.off("mouseenter", layerId, setPointerStyle);
mapRef.off("mouseleave", layerId, removePointerStyle);
});
};
}, [mapRef, mapboxProps]);

return (
<div className={cn("relative z-0 h-full w-full", className)}>
<ReactMapGL
Expand All @@ -152,8 +178,6 @@ export default function Map({
onMove={handleMapMove}
onLoad={handleMapLoad}
mapStyle={MAPBOX_STYLE}
maxZoom={10}
minZoom={0}
{...mapboxProps}
{...localViewState}
>
Expand Down
41 changes: 41 additions & 0 deletions client/src/components/map/popup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { PropsWithChildren } from "react";
import * as React from "react";

import { Popup } from "react-map-gl";

import { useAtom } from "jotai";
import { XIcon } from "lucide-react";

import { popupAtom } from "@/app/(overview)/store";

export default function MapPopup({ children }: PropsWithChildren) {
const [popup, setPopup] = useAtom(popupAtom);

if (!popup || !popup.features?.length) {
return null;
}

const closePopup = () => {
setPopup(null);
};

return (
<Popup
longitude={popup.lngLat.lng}
latitude={popup.lngLat.lat}
closeOnClick={false}
onClose={closePopup}
className="bg-transparent text-sm"
maxWidth="320"
>
<div className="flex flex-col gap-2">
<>
<button type="button" onClick={closePopup} className="self-end">
<XIcon className="h-5 w-5 text-foreground hover:text-muted-foreground" />
</button>
{children}
</>
</div>
</Popup>
);
}
37 changes: 37 additions & 0 deletions client/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from "react";

import { cva, type VariantProps } from "class-variance-authority";

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

const badgeVariants = cva(
"inline-flex items-center rounded-xl border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-big-stone-900 shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-accent-foreground hover:bg-slate-200",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-rose-700",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}

export { Badge, badgeVariants };
28 changes: 24 additions & 4 deletions client/src/containers/projects/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { ComponentProps, useState } from "react";

import { useSetAtom } from "jotai";
import { LayersIcon } from "lucide-react";

import { popupAtom } from "@/app/(overview)/store";

import Controls from "@/containers/projects/map/controls";
import ZoomControl from "@/containers/projects/map/controls/zoom";
import ProjectsLayer from "@/containers/projects/map/layers/projects";
import ProjectsLayer, {
LAYER_ID as COST_ABATEMENT_LAYER_ID,
} from "@/containers/projects/map/layers/projects";
import { MATRIX_COLORS } from "@/containers/projects/map/layers/projects/utils";
import MatrixLegend from "@/containers/projects/map/legend/types/matrix";
import CostAbatementPopup from "@/containers/projects/map/popup";

import Map from "@/components/map";
import { Button } from "@/components/ui/button";
Expand All @@ -24,9 +30,25 @@ export default function ProjectsMap() {
id: index,
}));

const setPopup = useSetAtom(popupAtom);

return (
<div className="h-full overflow-hidden rounded-t-2xl">
<Map>
<Map
minZoom={0}
maxZoom={10}
interactiveLayerIds={[COST_ABATEMENT_LAYER_ID]}
onClick={(e) => {
setPopup({
lngLat: e.lngLat,
features: e.features,
});
}}
>
<ProjectsLayer />

<CostAbatementPopup />

<Controls>
<ZoomControl />
</Controls>
Expand All @@ -53,8 +75,6 @@ export default function ProjectsMap() {
</PopoverContent>
</Popover>
</Controls>

<ProjectsLayer />
</Map>
</div>
);
Expand Down
9 changes: 6 additions & 3 deletions client/src/containers/projects/map/layers/projects/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { useGlobalFilters } from "@/app/(overview)/url-store";

import { generateColorRamp } from "@/containers/projects/map/layers/projects/utils";

export const LAYER_ID = "cost-abatement-layer";

export default function ProjectsLayer() {
const [filters] = useGlobalFilters();

Expand Down Expand Up @@ -52,7 +54,7 @@ export default function ProjectsLayer() {
const colors = generateColorRamp(COLOR_NUMBER);

const costAbatementLayer: FillLayerSpecification = {
id: "cost-abatement-layer",
id: LAYER_ID,
type: "fill",
source: costAbatementSource.id,

Expand Down Expand Up @@ -87,8 +89,9 @@ export default function ProjectsLayer() {

return (
<>
<Source {...costAbatementSource} />
<Layer {...costAbatementLayer} beforeId="custom-layers" />
<Source {...costAbatementSource}>
<Layer {...costAbatementLayer} beforeId="custom-layers" />
</Source>
</>
);
}
Expand Down
52 changes: 52 additions & 0 deletions client/src/containers/projects/map/popup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useAtomValue } from "jotai";

import { renderCurrency } from "@/lib/format";
import { cn } from "@/lib/utils";

import { popupAtom } from "@/app/(overview)/store";

import MapPopup from "@/components/map/popup";
import { Badge } from "@/components/ui/badge";

const HEADER_CLASSES = "py-0.5 text-left font-normal text-muted-foreground";
const CELL_CLASSES = "py-0.5 font-semibold";

export default function CostAbatementPopup() {
const popup = useAtomValue(popupAtom);

return (
<MapPopup>
<div className="flex items-center justify-between">
<span className="font-semibold">
{popup?.features?.[0]?.properties?.country}
</span>
<Badge variant="outline" className="pointer-events-none">
SUM
</Badge>
</div>
<table>
<thead>
<tr>
<th className={cn(HEADER_CLASSES, "pr-2")}>Cost</th>
<th className={cn(HEADER_CLASSES, "px-2")}>Abatement</th>
</tr>
</thead>
<tbody>
<tr>
<td className={cn(CELL_CLASSES, "pr-2")}>
{renderCurrency(popup?.features?.[0]?.properties?.cost)}
</td>
<td className={cn(CELL_CLASSES, "px-2")}>
{renderCurrency(
popup?.features?.[0]?.properties?.abatementPotential,
)}
</td>
</tr>
</tbody>
</table>
<p className="text-xs text-muted-foreground">
Values for the SUM of all projects.
</p>
</MapPopup>
);
}
29 changes: 29 additions & 0 deletions client/src/lib/format.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { cn } from "@/lib/utils";

export const formatCurrency = (
value: number,
options: Intl.NumberFormatOptions = {},
) => {
return Intl.NumberFormat(undefined, {
style: "currency",
currency: "USD",
...options,
}).format(value);
};

export function renderCurrency(
value: number,
options: Intl.NumberFormatOptions = {},
className?: HTMLSpanElement["className"],
) {
return (
<span
className={cn(
"inline-block first-letter:align-top first-letter:text-xs first-letter:tracking-[0.1rem] first-letter:text-muted-foreground",
className,
)}
>
{formatCurrency(value, options)}
</span>
);
}

0 comments on commit 727e68f

Please sign in to comment.