Skip to content

Commit

Permalink
Merge pull request #12 from Vizzuality/SS-46-basemap-with-settings-ce…
Browse files Browse the repository at this point in the history
…ntred-on-south-sudan

Add map settings
  • Loading branch information
clementprdhomme authored Oct 24, 2024
2 parents b7e4429 + 0254e22 commit 2e3fb17
Show file tree
Hide file tree
Showing 24 changed files with 545 additions and 24 deletions.
5 changes: 4 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"dependencies": {
"@artsy/fresnel": "7.1.4",
"@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.2",
"@radix-ui/react-radio-group": "1.2.1",
"@radix-ui/react-separator": "1.1.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-tooltip": "1.1.3",
Expand All @@ -21,7 +24,7 @@
"express": "4.21.1",
"mapbox-gl": "3.7.0",
"next": "14.2.15",
"nuqs": "1.20.0",
"nuqs": "2.0.3",
"pino-http": "10.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
Empty file removed client/public/.delete-later
Empty file.
Binary file added client/public/assets/images/basemap-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/public/assets/images/basemap-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/public/assets/images/basemap-satellite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Jost, DM_Serif_Text } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app";

import Head from "@/components/head";

Expand All @@ -17,7 +18,7 @@ export const metadata: Metadata = {

const jost = Jost({
subsets: ["latin"],
weight: ["300", "400", "600", "700", "800"],
weight: ["300", "400", "500", "600", "700", "800"],
variable: "--font-jost",
});

Expand All @@ -35,7 +36,9 @@ export default function RootLayout({
return (
<html lang="en" className={`${jost.variable} ${dmSerifText.variable}`}>
<Head />
<body>{children}</body>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}
34 changes: 34 additions & 0 deletions client/src/components/map/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BasemapStyle, LabelsStyle } from "./types";

import type { LngLatLike } from "react-map-gl";

export const DEFAULT_BOUNDS: [LngLatLike, LngLatLike] = [
Expand All @@ -14,3 +16,35 @@ export const MOBILE_MAX_BOUNDS: [LngLatLike, LngLatLike] = [
[19, -8],
[39, 23],
];

export const BASEMAPS: Record<BasemapStyle, { name: string; image: string }> = {
light: {
name: "Light",
image: "/assets/images/basemap-light.png",
},
dark: {
name: "Dark",
image: "/assets/images/basemap-dark.png",
},
satellite: {
name: "Satellite",
image: "/assets/images/basemap-satellite.png",
},
};

export const LABELS: Record<LabelsStyle, { name: string }> = {
light: {
name: "Light",
},
dark: {
name: "Dark",
},
"": {
name: "No labels",
},
};

export const DEFAULT_MAP_SETTINGS = {
basemap: BasemapStyle.Light,
labels: LabelsStyle.Light,
};
13 changes: 13 additions & 0 deletions client/src/components/map/controls/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import MapSettingsControls from "./map-settings";
import ZoomControls from "./zoom";

const Controls = () => {
return (
<div className="absolute bottom-10 right-5 z-10 flex flex-col gap-2 xl:bottom-6 xl:right-10">
<ZoomControls />
<MapSettingsControls />
</div>
);
};

export default Controls;
22 changes: 22 additions & 0 deletions client/src/components/map/controls/map-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import MapSettingsPanel from "@/components/panels/map-settings";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import GlobeIcon from "@/svgs/globe.svg";

const MapSettingsControls = () => {
return (
<Popover>
<PopoverTrigger asChild>
<Button type="button" variant="yellow" size="icon">
<span className="sr-only">Map settings</span>
<GlobeIcon aria-hidden />
</Button>
</PopoverTrigger>
<PopoverContent side="left" align="end" className="w-[250px]">
<MapSettingsPanel />
</PopoverContent>
</Popover>
);
};

export default MapSettingsControls;
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { Button } from "@/components/ui/button";
import MinusIcon from "@/svgs/minus.svg";
import PlusIcon from "@/svgs/plus.svg";

const Controls = () => {
const ZoomControls = () => {
const { current: map } = useMap();

const onClickZoomIn = useCallback(() => map?.zoomIn(), [map]);
const onClickZoomOut = useCallback(() => map?.zoomOut(), [map]);

return (
<div className="absolute bottom-6 right-10 z-10 flex flex-col gap-px">
<div className="flex flex-col gap-px">
<Button
type="button"
variant="yellow"
Expand All @@ -37,4 +37,4 @@ const Controls = () => {
);
};

export default Controls;
export default ZoomControls;
21 changes: 13 additions & 8 deletions client/src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactMapGL from "react-map-gl";

import { SIDEBAR_WIDTH } from "@/components/ui/sidebar";
import { env } from "@/env";
import useApplyMapSettings from "@/hooks/use-apply-map-settings";
import useBreakpoint from "@/hooks/use-breakpoint";
import useIsSidebarExpanded from "@/hooks/use-is-sidebar-expanded";
import useMapBounds from "@/hooks/use-map-bounds";
Expand All @@ -18,8 +19,10 @@ import type { MapRef, LngLatLike } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";

const Map = () => {
const isDesktop = useBreakpoint("xl", false, true);
const mapRef = useRef<MapRef>(null);
const [map, setMap] = useState<MapRef | null>(null);

const isDesktop = useBreakpoint("xl", false, true);

const isSidebarExpanded = useIsSidebarExpanded();
const previousIsSidebarExpanded = usePrevious(isSidebarExpanded);
Expand All @@ -44,17 +47,18 @@ const Map = () => {
}, [bounds, isDesktop, isSidebarExpanded]);

const onMove = useCallback(() => {
if (mapRef.current) {
setBounds(mapRef.current.getBounds()?.toArray() as [LngLatLike, LngLatLike]);
}
}, [mapRef, setBounds]);
setBounds(map?.getBounds()?.toArray() as [LngLatLike, LngLatLike]);
}, [map, setBounds]);

// Update the position of the map based on the sidebar's state
useEffect(() => {
if (isSidebarExpanded !== previousIsSidebarExpanded) {
mapRef.current?.fitBounds(bounds, initialViewState.fitBoundsOptions);
map?.fitBounds(bounds, initialViewState.fitBoundsOptions);
}
}, [isSidebarExpanded, previousIsSidebarExpanded, initialViewState, bounds]);
}, [map, isSidebarExpanded, previousIsSidebarExpanded, initialViewState, bounds]);

// Apply the basemap and labels
useApplyMapSettings(map);

return (
<ReactMapGL
Expand All @@ -66,6 +70,7 @@ const Map = () => {
mapStyle={env.NEXT_PUBLIC_MAPBOX_STYLE}
onMove={onMove}
logoPosition="bottom-right"
onLoad={() => setMap(mapRef.current)}
>
<Controls />
</ReactMapGL>
Expand Down
11 changes: 11 additions & 0 deletions client/src/components/map/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum BasemapStyle {
Light = "light",
Dark = "dark",
Satellite = "satellite",
}

export enum LabelsStyle {
Dark = "dark",
Light = "light",
NoLabels = "",
}
69 changes: 69 additions & 0 deletions client/src/components/panels/map-settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Image from "next/image";

import { BASEMAPS, LABELS } from "@/components/map/constants";
import { BasemapStyle, LabelsStyle } from "@/components/map/types";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import useMapBasemap from "@/hooks/use-map-basemap";
import useMapLabels from "@/hooks/use-map-labels";
import GlobeFilledIcon from "@/svgs/globe-filled.svg";

const MapSettingsPanel = () => {
const [basemap, setBasemap] = useMapBasemap();
const [labels, setLabels] = useMapLabels();

return (
<div className="pb-4 pt-1.5">
<fieldset className="border-b border-b-casper-blue-400 px-4 pb-3">
<legend className="flex items-center gap-1.5 text-sm leading-[26px]">
<GlobeFilledIcon className="shrink-0" />
Map style
</legend>
<RadioGroup
value={basemap}
onValueChange={(value) => setBasemap(value as BasemapStyle)}
className="mt-2 flex flex-col gap-2"
>
{Object.entries(BASEMAPS).map(([key, { name, image }]) => (
<div key={key} className="flex items-center gap-1 py-1">
<RadioGroupItem
variant="icon"
size="icon"
value={key}
id={`basemap-${key}`}
className="shrink-0"
>
<Image src={image} width={20} height={20} alt="" className="rounded-full" />
</RadioGroupItem>
<Label
htmlFor={`basemap-${key}`}
className="text-sm peer-hover:font-medium peer-hover:underline peer-focus-visible:font-medium peer-focus-visible:underline"
>
{name}
</Label>
</div>
))}
</RadioGroup>
</fieldset>
<fieldset className="mt-2 px-4">
<legend className="text-xs leading-6">Labels</legend>
<RadioGroup
value={labels}
onValueChange={(value) => setLabels(value as LabelsStyle)}
className="mt-1 flex items-center gap-6"
>
{Object.entries(LABELS).map(([key, { name }]) => (
<div key={key} className="flex items-center gap-2">
<RadioGroupItem value={key} id={`labels-${key}`} className="shrink-0" />
<Label htmlFor={`labels-${key}`} className="text-xs">
{name}
</Label>
</div>
))}
</RadioGroup>
</fieldset>
</div>
);
};

export default MapSettingsPanel;
4 changes: 2 additions & 2 deletions client/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const buttonVariants = cva(
default:
"bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 ring-offset-white focus-visible:ring-neutral-950",
yellow:
"bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400",
"bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400 data-[state=open]:bg-rhino-blue-900 data-[state=open]:hover:bg-rhino-blue-950 data-[state=open]:text-supernova-yellow-400",
ghost:
"bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 ring-offset-white focus-visible:ring-neutral-950",
},
size: {
default: "h-10 px-4 py-2",
icon: "h-10 w-10",
icon: "h-8 w-8 xl:h-10 xl:w-10",
auto: "",
},
},
Expand Down
19 changes: 19 additions & 0 deletions client/src/components/ui/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";

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

const labelVariants = cva("peer-disabled:cursor-not-allowed peer-disabled:opacity-70");

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;

export { Label };
31 changes: 31 additions & 0 deletions client/src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";

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

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 26, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-10 bg-white outline-none duration-500 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent };
Loading

0 comments on commit 2e3fb17

Please sign in to comment.