Skip to content

Commit

Permalink
Merge pull request #29
Browse files Browse the repository at this point in the history
Allow to search and zoom onto locations
  • Loading branch information
clementprdhomme authored Nov 18, 2024
2 parents 2b88cd6 + e6ca714 commit 06308c1
Show file tree
Hide file tree
Showing 28 changed files with 1,881 additions and 37 deletions.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
"@radix-ui/react-tooltip": "1.1.3",
"@t3-oss/env-nextjs": "0.11.1",
"@tanstack/react-query": "5.59.16",
"@turf/bbox": "7.1.0",
"@types/mapbox-gl": "3.4.0",
"apng-js": "1.1.4",
"axios": "1.7.7",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"cmdk": "1.0.0",
"date-fns": "4.1.0",
"express": "4.21.1",
"mapbox-gl": "3.7.0",
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DeckglMapboxProvider from "@/components/map/deckgl-mapbox-provider";
import LayerManager from "@/components/map/layer-manager";
import { SIDEBAR_WIDTH } from "@/components/ui/sidebar";
import { env } from "@/env";
import useApplyMapLocation from "@/hooks/use-apply-map-location";
import useApplyMapSettings from "@/hooks/use-apply-map-settings";
import useBreakpoint from "@/hooks/use-breakpoint";
import useIsSidebarExpanded from "@/hooks/use-is-sidebar-expanded";
Expand Down Expand Up @@ -82,6 +83,9 @@ const Map = () => {
// Apply the basemap and labels
useApplyMapSettings(map);

// Zoom the map on the selected location
useApplyMapLocation(map);

return (
<ReactMapGL
ref={mapRef}
Expand Down
35 changes: 34 additions & 1 deletion client/src/components/navigation/navigation-desktop/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
"use client";

import { useCallback, useState } from "react";

import Intro from "@/components/intro";
import LocationPanel from "@/components/panels/location";
import MainPanel from "@/components/panels/main";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Sidebar, SidebarContent, SidebarHeader, SidebarTrigger } from "@/components/ui/sidebar";
import useLocation from "@/hooks/use-location";
import { useLocationByCode } from "@/hooks/use-location-by-code";
import MapPinIcon from "@/svgs/map-pin.svg";

import Logo from "../logo";

const NavigationDesktop = () => {
const [locationDialogOpen, setLocationDialogOpen] = useState(false);

const [location] = useLocation();
const { data, isLoading } = useLocationByCode(location.code.slice(-1)[0]);

const onExitLocationDialog = useCallback(() => {
setLocationDialogOpen(false);
}, []);

return (
<>
<Logo />
Expand All @@ -15,8 +32,24 @@ const NavigationDesktop = () => {
<SidebarTrigger className="absolute right-0 top-6 z-10 translate-x-1/2 transition-transform group-data-[state=collapsed]:translate-x-full [&_svg]:rotate-90 group-data-[state=collapsed]:[&_svg]:-rotate-90" />
</SidebarHeader>
<SidebarContent className="overflow-auto">
<div className="bg-rhino-blue-900 px-10 pb-8 text-white">
<div className="bg-rhino-blue-900 px-10 pb-5 text-white">
<Intro />
<Dialog open={locationDialogOpen} onOpenChange={setLocationDialogOpen}>
<DialogTrigger asChild>
<Button
type="button"
variant="ghost"
className="relative -left-4 mt-2 gap-4 text-white/60 hover:text-white focus-visible:text-white"
>
{(!!isLoading || !data) && "Select location"}
{!isLoading && !!data && data.name}
<MapPinIcon aria-hidden />
</Button>
</DialogTrigger>
<DialogContent className="gap-0">
<LocationPanel onExit={onExitLocationDialog} />
</DialogContent>
</Dialog>
</div>
<MainPanel />
</SidebarContent>
Expand Down
8 changes: 6 additions & 2 deletions client/src/components/navigation/navigation-mobile/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useCallback, useState } from "react";

import Intro from "@/components/intro";
import LocationPanel from "@/components/panels/location";
Expand All @@ -14,6 +14,10 @@ import { Tab } from "./types";
const NavigationMobile = () => {
const [tab, setTab] = useState<Tab>(Tab.Main);

const onExitLocationPanel = useCallback(() => {
setTab(Tab.Map);
}, [setTab]);

return (
<>
<Sheet modal={false} open={tab === Tab.Main || tab === Tab.Location}>
Expand All @@ -28,7 +32,7 @@ const NavigationMobile = () => {
</SheetHeader>
<div className="mt-6">
{tab === Tab.Main && <MainPanel />}
{tab === Tab.Location && <LocationPanel />}
{tab === Tab.Location && <LocationPanel onExit={onExitLocationPanel} />}
</div>
</SheetContent>
</Sheet>
Expand Down
202 changes: 202 additions & 0 deletions client/src/components/panels/location/administrative-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { useCallback, useMemo } from "react";

import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxLoading,
ComboboxTrigger,
} from "@/components/ui/combobox";
import useLocationsByType from "@/hooks/use-locations-by-type";
import XMarkIcon from "@/svgs/xmark.svg";

interface AdministrativeTabProps {
locationCode: string[];
onChangeLocationCode: (locationCode: string[]) => void;
}

const AdministrativeTab = ({ locationCode, onChangeLocationCode }: AdministrativeTabProps) => {
const { data: dataLevel1, isLoading: isLoadingLevel1 } = useLocationsByType("administrative", 1);
const { data: dataLevel2, isLoading: isLoadingLevel2 } = useLocationsByType(
"administrative",
2,
locationCode[0],
locationCode.length > 0,
);
const { data: dataLevel3, isLoading: isLoadingLevel3 } = useLocationsByType(
"administrative",
3,
locationCode?.[1],
locationCode.length > 1,
);

const selectedLocationLevel1 = useMemo(() => {
if (locationCode.length === 0 || isLoadingLevel1) {
return undefined;
}

return dataLevel1.find(({ code }) => code === locationCode[0]);
}, [isLoadingLevel1, dataLevel1, locationCode]);

const selectedLocationLevel2 = useMemo(() => {
if (locationCode.length < 2 || isLoadingLevel2) {
return undefined;
}

return dataLevel2.find(({ code }) => code === locationCode[1]);
}, [isLoadingLevel2, dataLevel2, locationCode]);

const selectedLocationLevel3 = useMemo(() => {
if (locationCode.length < 3 || isLoadingLevel3) {
return undefined;
}

return dataLevel3.find(({ code }) => code === locationCode[2]);
}, [isLoadingLevel3, dataLevel3, locationCode]);

const onChangeLocationLevel1 = useCallback(
(code: string | undefined) => {
onChangeLocationCode(code ? [code] : []);
},
[onChangeLocationCode],
);

const onChangeLocationLevel2 = useCallback(
(code: string | undefined) => {
onChangeLocationCode(code ? [locationCode[0], code] : [locationCode[0]]);
},
[locationCode, onChangeLocationCode],
);

const onChangeLocationLevel3 = useCallback(
(code: string | undefined) => {
onChangeLocationCode(
code ? [locationCode[0], locationCode[1], code] : [locationCode[0], locationCode[1]],
);
},
[locationCode, onChangeLocationCode],
);

return (
<>
<p>
Select a location by administrative boundaries to view data specific to states, counties, or
regions. This helps focus on localized insights for effective resource management and
planning.
</p>
<div className="mt-8 flex flex-col gap-4">
<Combobox value={locationCode[0] ?? ""} onValueChange={onChangeLocationLevel1}>
<div className="relative">
<ComboboxTrigger>
{!!selectedLocationLevel1 && (
<span className="font-semibold">{selectedLocationLevel1.name}</span>
)}
{!selectedLocationLevel1 && "Select state"}
</ComboboxTrigger>
{!!selectedLocationLevel1 && (
<Button
type="button"
variant="ghost"
size="auto"
className="absolute right-3 top-1/2 size-6 -translate-y-1/2 rounded-full bg-casper-blue-400 text-rhino-blue-950 focus-visible:ring-casper-blue-950"
onClick={() => onChangeLocationLevel1(undefined)}
>
<span className="sr-only">Clear state</span>
<XMarkIcon aria-hidden />
</Button>
)}
</div>
<ComboboxContent>
<ComboboxInput placeholder="Search state" />
<ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
{isLoadingLevel1 && <ComboboxLoading>Loading...</ComboboxLoading>}
{!isLoadingLevel1 &&
dataLevel1.map(({ code, name }) => (
<ComboboxItem key={code} value={code} keywords={[name]}>
{name}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
<Combobox value={locationCode?.[1] ?? ""} onValueChange={onChangeLocationLevel2}>
<div className="relative">
<ComboboxTrigger disabled={locationCode.length < 1}>
{!!selectedLocationLevel2 && (
<span className="font-semibold">{selectedLocationLevel2.name}</span>
)}
{!selectedLocationLevel2 && "Select countie"}
</ComboboxTrigger>
{!!selectedLocationLevel2 && (
<Button
type="button"
variant="ghost"
size="auto"
className="absolute right-3 top-1/2 size-6 -translate-y-1/2 rounded-full bg-casper-blue-400 text-rhino-blue-950 focus-visible:ring-casper-blue-950"
onClick={() => onChangeLocationLevel2(undefined)}
>
<span className="sr-only">Clear countie</span>
<XMarkIcon aria-hidden />
</Button>
)}
</div>
<ComboboxContent>
<ComboboxInput placeholder="Search countie" />
<ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
{isLoadingLevel2 && <ComboboxLoading>Loading...</ComboboxLoading>}
{!isLoadingLevel2 &&
dataLevel2.map(({ code, name }) => (
<ComboboxItem key={code} value={code} keywords={[name]}>
{name}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
<Combobox value={locationCode?.[2] ?? ""} onValueChange={onChangeLocationLevel3}>
<div className="relative">
<ComboboxTrigger disabled={locationCode.length < 2}>
{!!selectedLocationLevel3 && (
<span className="font-semibold">{selectedLocationLevel3.name}</span>
)}
{!selectedLocationLevel3 && "Select region"}
</ComboboxTrigger>
{!!selectedLocationLevel3 && (
<Button
type="button"
variant="ghost"
size="auto"
className="absolute right-3 top-1/2 size-6 -translate-y-1/2 rounded-full bg-casper-blue-400 text-rhino-blue-950 focus-visible:ring-casper-blue-950"
onClick={() => onChangeLocationLevel3(undefined)}
>
<span className="sr-only">Clear region</span>
<XMarkIcon aria-hidden />
</Button>
)}
</div>
<ComboboxContent>
<ComboboxInput placeholder="Search region" />
<ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
{isLoadingLevel3 && <ComboboxLoading>Loading...</ComboboxLoading>}
{!isLoadingLevel3 &&
dataLevel3.map(({ code, name }) => (
<ComboboxItem key={code} value={code} keywords={[name]}>
{name}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>{" "}
</div>
</>
);
};

export default AdministrativeTab;
Loading

0 comments on commit 06308c1

Please sign in to comment.