Skip to content

Commit

Permalink
Add support for Radar maps
Browse files Browse the repository at this point in the history
  • Loading branch information
devcshort committed Jun 8, 2024
1 parent 5e3c696 commit 07e8a43
Show file tree
Hide file tree
Showing 20 changed files with 833 additions and 86 deletions.
381 changes: 378 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@
"lodash": "^4.17.21",
"lucide-react": "^0.381.0",
"mapbox-gl": "^3.4.0",
"maplibre-gl": "^4.3.2",
"nanoid": "^5.0.7",
"next": "^14.2.3",
"next-auth": "^4.23.2",
"next-i18next": "^15.3.0",
"next-themes": "^0.3.0",
"nookies": "^2.5.2",
"radar-sdk-js": "^4.3.0",
"react": "^18.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
Expand Down
20 changes: 17 additions & 3 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { useAppConfig } from '@/hooks/use-app-config';
import { ISearchResult } from '@/types/search-result';
import { LngLatLike } from 'maplibre-gl';
import dynamic from 'next/dynamic';
import { memo } from 'react';

export default function MapLoader(props) {
interface IMapLoaderProps {
results?: ISearchResult[];
center?: LngLatLike;
zoom?: number;
animate?: boolean;
boundsPadding?: number;
boundsZoom?: number;
}

const MapLoader = memo<IMapLoaderProps>(function MemoizedMapLoader(props) {
const appConfig = useAppConfig();
const mapAdapterName = appConfig.adapters.map;
const MapComponent = dynamic(() =>
const MapComponent = dynamic<IMapLoaderProps>(() =>
import(`./${mapAdapterName}`).then((mod) => mod.default),
);

return <MapComponent {...props} />;
}
});

export default MapLoader;
5 changes: 3 additions & 2 deletions src/components/map/mapbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ export default function MapboxMap(props) {
zoom: 12,
animate: false,
boundsPadding: 50,
...props,
}),
[appConfig?.features?.map?.center, MAPBOX_ACCESS_TOKEN],
[appConfig?.features?.map?.center, MAPBOX_ACCESS_TOKEN, props],
);

return (
<Map {...props} {...mapProps}>
<Map {...mapProps}>
<Markers results={props?.results ?? []} />
</Map>
);
Expand Down
91 changes: 91 additions & 0 deletions src/components/map/radar/components/map-markers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ISearchResult } from '@/types/search-result';
import { memo } from 'react';
import Marker from './marker';
import RenderHtml from '@/components/render-html';
import { ReferralButton } from '@/components/referral-button';
import { Globe, Phone } from 'lucide-react';
import { useTranslation } from 'next-i18next';

export const Markers = memo(function MemoizedMarkers({
results,
zoom,
}: {
results: ISearchResult[];
zoom?: number;
}) {
const { t } = useTranslation();

if (!results || results.length === 0) return null;

return (
<>
{results.map((result) => {
if (result?.location?.point?.coordinates == null) return null;

return (
<Marker
key={result.id}
longitude={result.location.point.coordinates[0]}
latitude={result.location.point.coordinates[1]}
className="custom-marker"
zoom={zoom}
onClick={(e, marker) => {
e.preventDefault();
e.stopPropagation();
const element = document.getElementById(result.id);

document
.querySelectorAll('.outline')
.forEach((elem) => elem.classList.remove('outline'));

if (element) {
element.classList.add('outline');
element.scrollIntoView();
}
}}
popup={
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">{result.name}</h3>

<div className="text-sm">
<RenderHtml html={result?.description} />
</div>

<div className="flex gap-2">
<ReferralButton
referralType="call_referral"
resourceId={result.id}
resource={result}
disabled={!result.phone}
href={`tel:${result.phone}`}
className="w-full min-w-[130px] gap-1"
>
<Phone className="size-4" />
{t('call_to_action.call', {
ns: 'common',
})}
</ReferralButton>

<ReferralButton
referralType="website_referral"
resourceId={result.id}
resource={result}
disabled={!result.website}
href={result?.website ?? ''}
target="_blank"
className="w-full min-w-[130px] gap-1"
>
<Globe className="size-4" />
{t('call_to_action.view_website', {
ns: 'common',
})}
</ReferralButton>
</div>
</div>
}
/>
);
})}
</>
);
});
45 changes: 45 additions & 0 deletions src/components/map/radar/components/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Radar from 'radar-sdk-js';
import RadarMap from 'radar-sdk-js/dist/ui/RadarMap';
import { useEffect, useState } from 'react';
import { MapContextProvider } from '../mapContext';
import 'radar-sdk-js/dist/radar.css';

export default function Map({
children,
publishableKey,
zoom,
center,
}: {
children?: React.ReactNode;
publishableKey: string;
zoom?: number;
center?: [number, number];
}) {
const [map, setMap] = useState<RadarMap | undefined>();

useEffect(() => {
Radar.initialize(publishableKey);
const _map = Radar.ui.map({
container: 'map',
style: 'radar-default-v1',
zoom: zoom || 0,
center,
});
setMap(_map);

return () => {
_map.remove();
setMap(undefined);
};
}, [publishableKey, zoom, center]);

return (
<MapContextProvider value={{ map }}>
<div id="map-container" className="aboslute h-full w-full">
<div id="map" className="absolute h-full w-full">
{children}
</div>
</div>
</MapContextProvider>
);
}
61 changes: 61 additions & 0 deletions src/components/map/radar/components/marker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ReactElement, useContext, useEffect } from 'react';
import { mapContext } from '../mapContext';
import Radar from 'radar-sdk-js';
import { renderToStaticMarkup } from 'react-dom/server';
import RadarMarker from 'radar-sdk-js/dist/ui/RadarMarker';

export default function Marker({
latitude,
longitude,
popup,
className,
onClick,
zoom,
}: {
latitude?: number;
longitude?: number;
popup?: ReactElement;
className?: string;
onClick?: (e: MouseEvent, marker: RadarMarker) => void;
zoom?: number;
}) {
const { map } = useContext(mapContext);

useEffect(() => {
if (map == null) return;
if (latitude == null || longitude == null) return;

const container = popup ? document.createElement('div') : undefined;
if (container != null && popup != null) {
container.innerHTML = renderToStaticMarkup(popup);
}

const marker = Radar.ui
.marker({
popup: {
element: container,
maxWidth: '320px',
},
})
.setLngLat([longitude, latitude])
.addTo(map);

if (className != null) {
marker.getElement().classList.add(className);
}

const clickListener = (e: any) => {
return onClick?.(e, marker);
};

marker.getElement().addEventListener('click', clickListener);

map?.fitToMarkers({ animate: false });

return () => {
marker.remove();
};
}, [longitude, latitude, map, popup, className, onClick, zoom]);

return null;
}
25 changes: 25 additions & 0 deletions src/components/map/radar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMemo } from 'react';
import { getPublicConfig } from '@/pages/api/config';
import Map from './components/map';
import { useAppConfig } from '@/hooks/use-app-config';
import { Markers } from './components/map-markers';

export default function RadarMap(props) {
const appConfig = useAppConfig();
const RADAR_ACCESS_TOKEN = getPublicConfig('RADAR_ACCESS_TOKEN');

const mapProps = useMemo(
() => ({
publishableKey: RADAR_ACCESS_TOKEN,
center: appConfig?.features?.map?.center,
zoom: 12,
}),
[RADAR_ACCESS_TOKEN, appConfig],
);

return (
<Map {...mapProps} {...props}>
<Markers results={props.results} zoom={props.boundsZoom} />
</Map>
);
}
9 changes: 9 additions & 0 deletions src/components/map/radar/mapContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import RadarMap from 'radar-sdk-js/dist/ui/RadarMap';
import { createContext } from 'react';

interface IMapContext {
map?: RadarMap;
}

export const mapContext = createContext<IMapContext>({});
export const MapContextProvider = mapContext.Provider;
10 changes: 4 additions & 6 deletions src/components/resource/components/information/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useAppConfig } from '@/hooks/use-app-config';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import Addresses from './addresses';
import PhoneNumbers from './phone-numbers';
Expand All @@ -11,7 +10,6 @@ import Eligibility from './eligibility';
import Fees from './fees';
import Languages from './languages';
import ServiceArea from './service-area';
import { getPublicConfig } from '@/pages/api/config';
import { IResource } from '@/types/resource';
import MapLoader from '@/components/map';

Expand All @@ -20,9 +18,6 @@ type Props = {
};

export default function ResourceInformation(props: Props) {
const appConfig = useAppConfig();
const MAPBOX_ACCESS_TOKEN = getPublicConfig('MAPBOX_ACCESS_TOKEN');

return (
<Card>
<CardContent className="p-0 pb-2">
Expand All @@ -33,9 +28,12 @@ export default function ResourceInformation(props: Props) {
<div className="flex h-full w-full">
<MapLoader
zoom={12}
marker={{ zoom: 12 }}
boundsPadding={50}
boundsZoom={13}
results={[{ location: { point: props.data.location } }]}
results={[
{ id: props.data.id, location: { point: props.data.location } },
]}
/>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/results/components/result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export function Result(props: Props) {
) : (
<Tooltip>
<TooltipTrigger>
<Badge>{t('search.address_unavailable')}</Badge>
<Badge variant="outline">
{t('search.address_unavailable')}
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-52 shadow-md" side="right">
<p>{t('search.confidential_address')}</p>
Expand Down
Loading

0 comments on commit 07e8a43

Please sign in to comment.