Skip to content

Commit

Permalink
feat: interactive map (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
mortennordseth authored Oct 5, 2023
1 parent 8f187ea commit 6dba636
Show file tree
Hide file tree
Showing 18 changed files with 686 additions and 67 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@apollo/client": "^3.8.4",
"@atb-as/theme": "^7.1.1",
"@leile/lobo-t": "^1.0.5",
"@react-aria/focus": "^3.14.2",
"@types/node": "20.6.2",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
Expand Down
6 changes: 3 additions & 3 deletions src/components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {MouseEventHandler} from 'react';
import {ButtonBase, ButtonBaseProps, getBaseButtonClassName} from './utils';
import React, { MouseEventHandler } from 'react';
import { ButtonBase, ButtonBaseProps, getBaseButtonClassName } from './utils';

export type ButtonProps = {
/** Action when clicked */
Expand All @@ -15,7 +15,7 @@ export type ButtonProps = {
} & ButtonBaseProps;

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
function Button({onClick, testID, buttonProps, ...props}, ref) {
function Button({ onClick, testID, buttonProps, ...props }, ref) {
const className = getBaseButtonClassName(props);
return (
<button
Expand Down
50 changes: 50 additions & 0 deletions src/components/map/__tests__/map.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { MapHeader } from '@atb/components/map';
import { MapHeaderProps } from '@atb/components/map/map-header';
import { FeatureCategory } from '@atb/components/venue-icon';

const stopPlaceMock: MapHeaderProps = {
layer: 'venue',
name: 'Trondheim S',
id: 'NSR:StopPlace:41742',
street: 'Fosenkaia',
category: [FeatureCategory.ONSTREET_BUS],
};

const addressMock: MapHeaderProps = {
layer: 'address',
id: '44523952221',
name: 'Prinsens gate',
};

afterEach(function () {
cleanup();
});

describe('MapHeader', function () {
it('should render travel to and from links for stop place', async () => {
const output = render(<MapHeader {...stopPlaceMock} />);

expect(output.getByText('Reis fra')).toBeInTheDocument();
expect(output.getByText('Reis til')).toBeInTheDocument();
});

it('should not render travel to and from links for address', async () => {
const output = render(<MapHeader {...addressMock} />);

expect(output.queryByText('Reis fra')).not.toBeInTheDocument();
expect(output.queryByText('Reis til')).not.toBeInTheDocument();
});

it('should show icons for travel method', async () => {
const output = render(
<MapHeader
{...stopPlaceMock}
category={[FeatureCategory.ONSTREET_BUS, FeatureCategory.ONSTREET_TRAM]}
/>,
);
expect(output.getByAltText('Buss')).toBeInTheDocument();
expect(output.getByAltText('Trikk')).toBeInTheDocument();
});
});
48 changes: 3 additions & 45 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,3 @@
import React, { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import style from './map.module.css';
import 'mapbox-gl/dist/mapbox-gl.css';
import { mapboxData } from '@atb/modules/org-data';

export type Position = {
lat: number;
lng: number;
};

export type MapProps = {
position?: Position;
};

export function Map({ position = defaultPosition }: MapProps) {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map>();

useEffect(() => {
if (!mapContainer.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
accessToken: mapboxData.accessToken,
style: mapboxData.style,
center: [position.lng, position.lat],
zoom: 13,
});

return () => map.current?.remove();
}, []); // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
if (map.current) {
map.current.setCenter([position.lng, position.lat]);
}
}, [position.lng, position.lat]);

return <div ref={mapContainer} className={style.mapContainer} />;
}

const defaultPosition: Position = {
lat: mapboxData.defaultLat,
lng: mapboxData.defaultLng,
};
export { Map } from './map';
export { MapWithHeader } from './map-with-header';
export { MapHeader } from './map-header';
81 changes: 81 additions & 0 deletions src/components/map/map-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import style from './map.module.css';

import { ButtonLink } from '@atb/components/button';
import { ComponentText, useTranslation } from '@atb/translations';
import VenueIcon, { FeatureCategory } from '@atb/components/venue-icon';
import { and } from '@atb/utils/css';
import { MonoIcon } from '@atb/components/icon';

export type MapHeaderProps = {
id: string;
name: string; // StopPlace name or address
layer: 'address' | 'venue';
street?: string;
category?: FeatureCategory[];
};

export function MapHeader({
id,
name,
layer,
street,
category,
}: MapHeaderProps) {
const { t } = useTranslation();
return (
<div className={style.header}>
<div className={style.header__leftContainer}>
<div className={style.header__icons}>
{layer === 'address' || !category ? (
<div>
<MonoIcon size="large" icon="map/Pin" overrideMode="dark" />
</div>
) : (
category.map((type) => (
<div key={[type, 'icon'].join('-')}>
<VenueIcon category={[type]} size="large" overrideMode="dark" />
</div>
))
)}
</div>
<div className={style.header__info}>
<h3
className={and(
'typo-heading--medium',
layer === 'address' && style['flexOrder__2'],
)}
>
{name}
</h3>
<p
className={and(
'typo-body__secondary',
style['header__info__secondary'],
)}
>
{layer === 'venue'
? t(ComponentText.Map.header.venue(street || '?')) // @TODO: better types
: t(ComponentText.Map.header.address)}
</p>
</div>
</div>

{layer === 'venue' && (
<div className={style.header__buttons}>
<ButtonLink
mode="interactive_0"
href={`/planner?travelFrom=${id}`}
title={t(ComponentText.Map.button.travelFrom)}
className={style.header__button}
/>
<ButtonLink
mode="interactive_0"
href={`/planner?travelTo=${id}`}
title={t(ComponentText.Map.button.travelTo)}
className={style.header__button}
/>
</div>
)}
</div>
);
}
15 changes: 15 additions & 0 deletions src/components/map/map-with-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import style from './map.module.css';
import { MapHeader, Map } from '.';
import { MapHeaderProps } from './map-header';
import { MapProps } from './map';

export type MapWithHeaderProps = MapHeaderProps & MapProps;

export function MapWithHeader({ ...props }: MapWithHeaderProps) {
return (
<div className={style.mapWithHeader}>
<MapHeader {...props} />
<Map {...props} />
</div>
);
}
134 changes: 133 additions & 1 deletion src/components/map/map.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,136 @@
.mapContainer {
.mapWithHeader {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
gap: var(--spacings-medium);
background: var(--static-background-background_0-background);
color: var(--static-background-background_0-text);
border-radius: var(--border-radius-regular) var(--border-radius-regular) 0 0;
padding: var(--spacings-medium);
}

.header__icons {
display: flex;
gap: var(--spacings-medium);
}

.header__icons div {
background-color: var(--static-background-background_accent_0-background);
color: var(--static-background-background_accent_0-text);
padding: var(--spacings-small);
border-radius: var(--border-radius-regular);
display: flex;
justify-content: center;
align-items: center;
}

.header__info {
display: flex;
flex-direction: column;
}

.header__info__secondary {
color: var(--text-colors-secondary);
}

.flexOrder__2 {
order: 2;
}

.header__buttons {
display: flex;
gap: var(--spacings-medium);
margin-left: auto;
}

.mapContainer {
position: relative;
height: 100%;
width: 100%;
}

.header__leftContainer {
display: flex;
gap: var(--spacings-medium);
}

.map {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}

.mapWrapper {
position: relative;
height: 100%;
width: 100%;
}

.fullscreenButton {
display: none;
}

.closeButton {
z-index: 250;
position: absolute;
top: var(--spacings-xLarge);
left: var(--spacings-xLarge);
display: none;
}

.closeButton img {
display: block;
}

.buttonsContainer {
position: absolute;
right: var(--spacings-xLarge);
bottom: 3rem;
z-index: 250;
}

.pinSvg__fill {
fill: var(--interactive-interactive_0-default-background);
}

@media only screen and (max-width: 650px) {
.header {
flex-direction: column;
background: inherit;
color: inherit;
padding: var(--spacings-medium) 0;
}
.header__buttons {
margin-left: 0;
width: 100%;
align-items: center;
}
.header__button {
width: 100%;
text-align: center;
}
.fullscreenButton {
display: flex;
justify-content: center;
border: 2px solid #000000;
}
.fullscreenButton span {
flex: none;
}
.closeButton {
display: block;
}
.mapWrapper {
z-index: 250;
display: none;
position: fixed;
top: 0;
left: 0;
}
}
Loading

0 comments on commit 6dba636

Please sign in to comment.