diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 0afbbb3..c050be3 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -20,6 +20,20 @@ export const parameters = { backgrounds: { disable: true, }, + options: { + storySort: { + order: [ + 'Introduction', + 'Alternative*', + 'Coordinate*', + 'Global', + 'React', + 'React-Leaflet', + 'Components', + 'Patterns', + ], + }, + }, viewport: { viewports, }, diff --git a/package-lock.json b/package-lock.json index 4a1d900..9d6c3ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@amsterdam/design-system-assets": "^0.2.0", "@amsterdam/design-system-css": "^0.8.0", "@amsterdam/design-system-react": "^0.8.0", + "@amsterdam/design-system-react-icons": "^0.1.12", "@amsterdam/design-system-tokens": "^0.8.0", "leaflet": "^1.9.4", "proj4": "^2.11.0", diff --git a/package.json b/package.json index 435a2dd..336c686 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@amsterdam/design-system-assets": "^0.2.0", "@amsterdam/design-system-css": "^0.8.0", "@amsterdam/design-system-react": "^0.8.0", + "@amsterdam/design-system-react-icons": "^0.1.12", "@amsterdam/design-system-tokens": "^0.8.0", "leaflet": "^1.9.4", "proj4": "^2.11.0", diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx new file mode 100644 index 0000000..ffe5ce0 --- /dev/null +++ b/src/components/Map/Map.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from 'react'; +import L from 'leaflet'; +import { MapContext } from './MapContext'; +import { DEFAULT_MAP_OPTIONS } from './defaultMapOptions'; +import 'leaflet/dist/leaflet.css'; +import styles from './MapStyles.module.css'; + +export type MapProps = { + mapOptions?: L.MapOptions; + children?: React.ReactNode; +}; + +export default function Map({ + mapOptions = DEFAULT_MAP_OPTIONS, + children, +}: MapProps) { + const mapRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const createdMapInstance = useRef(false); + + useEffect(() => { + if (mapRef.current === null || createdMapInstance.current !== false) { + return; + } + + createdMapInstance.current = true; + const map = new L.Map(mapRef.current, { + ...DEFAULT_MAP_OPTIONS, + ...mapOptions, + }); + L.tileLayer(`https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png`, { + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }).addTo(map); + + // Remove Leaflet link from the map + map.attributionControl.setPrefix(false); + + setMapInstance(map); + + return () => { + mapInstance?.remove(); + }; + }, [mapInstance, mapOptions, mapRef]); + + return ( +
+ {!!mapInstance && ( + + {children} + + )} +
+ ); +} diff --git a/src/components/Map/MapContext.ts b/src/components/Map/MapContext.ts new file mode 100644 index 0000000..35d8c02 --- /dev/null +++ b/src/components/Map/MapContext.ts @@ -0,0 +1,16 @@ +import type { Map } from 'leaflet'; +import { createContext, useContext } from 'react'; + +export const MapContext = createContext<{ mapInstance: Map | null }>({ + mapInstance: null, +}); + +export function useMapInstance() { + const { mapInstance } = useContext(MapContext); + + if (mapInstance === null) { + throw Error('Fout, geen mapinstance gevonden in context.'); + } + + return mapInstance; +} diff --git a/src/components/Map/MapStyles.module.css b/src/components/Map/MapStyles.module.css new file mode 100644 index 0000000..6f6e01a --- /dev/null +++ b/src/components/Map/MapStyles.module.css @@ -0,0 +1,5 @@ +.container { + height: 100%; + position: relative; + width: 100%; +} diff --git a/src/components/Map/defaultMapOptions.ts b/src/components/Map/defaultMapOptions.ts new file mode 100644 index 0000000..c85c043 --- /dev/null +++ b/src/components/Map/defaultMapOptions.ts @@ -0,0 +1,16 @@ +import getCrsRd from '@/utils/getCrsRd'; + +export const DEFAULT_MAP_OPTIONS = { + center: [52.370216, 4.895168] as [number, number], + zoom: 12, + zoomControl: false, + maxZoom: 16, + minZoom: 3, + // Ensure proper handling for Rijksdriehoekcoördinaten + crs: getCrsRd(), + // Prevent the user browsing too far outside Amsterdam otherwise the map will render blank greyspace. Amsterdam tile layer only supports Amsterdam and the immediate surrounding areas + maxBounds: [ + [52.25168, 4.64034], + [52.50536, 5.10737], + ] as [number, number][], +}; diff --git a/src/components/ZoomControl/ZoomControl.tsx b/src/components/ZoomControl/ZoomControl.tsx new file mode 100644 index 0000000..509c6c1 --- /dev/null +++ b/src/components/ZoomControl/ZoomControl.tsx @@ -0,0 +1,39 @@ +import { Button, Icon, VisuallyHidden } from '@amsterdam/design-system-react'; +import { FunctionComponent } from 'react'; +import { + EnlargeIcon, + MinimiseIcon, +} from '@amsterdam/design-system-react-icons'; + +import styles from './styles.module.css'; +import { useMapInstance } from '@/components/Map/MapContext'; + +const ZoomControl: FunctionComponent = () => { + const mapInstance = useMapInstance(); + + const handleZoomInClick = () => { + if (mapInstance) { + mapInstance?.setZoom(mapInstance.getZoom() + 1); + } + }; + const handleZoomOutClick = () => { + if (mapInstance) { + mapInstance?.setZoom(mapInstance.getZoom() - 1); + } + }; + + return ( +
+ + +
+ ); +}; + +export default ZoomControl; diff --git a/src/components/ZoomControl/styles.module.css b/src/components/ZoomControl/styles.module.css new file mode 100644 index 0000000..9c6014b --- /dev/null +++ b/src/components/ZoomControl/styles.module.css @@ -0,0 +1,10 @@ +.buttons { + bottom: var(--ams-space-inside-lg, 24px); + display: inline-flex; + flex-direction: column; + gap: var(--ams-space-inside-xs, 8px); + position: absolute; + right: var(--ams-space-inside-lg, 24px); + /* TODO Temporary fix, need to hook into control layer */ + z-index: 999; +} diff --git a/src/pages/Fullscreen/FullscreenPage.tsx b/src/pages/Fullscreen/FullscreenPage.tsx new file mode 100644 index 0000000..4c0cd0d --- /dev/null +++ b/src/pages/Fullscreen/FullscreenPage.tsx @@ -0,0 +1,25 @@ +import { Screen, SkipLink } from '@amsterdam/design-system-react'; +import { ReactElement } from 'react'; + +export type FullscreenPageProps = { + header?: ReactElement; + map?: ReactElement; + footer?: ReactElement; +}; + +export const FullscreenPage = ({ + header, + map, + footer, +}: FullscreenPageProps) => { + return ( + <> + Direct naar inhoud + + {header} + {map} + {footer} + + + ); +}; diff --git a/src/pages/Fullscreen/FullscreenPageFooter.tsx b/src/pages/Fullscreen/FullscreenPageFooter.tsx new file mode 100644 index 0000000..511e4f3 --- /dev/null +++ b/src/pages/Fullscreen/FullscreenPageFooter.tsx @@ -0,0 +1,187 @@ +import { + Column, + Footer, + Grid, + Heading, + Link, + LinkList, + PageMenu, + Paragraph, + VisuallyHidden, +} from '@amsterdam/design-system-react'; +import { ChattingIcon, PhoneIcon } from '@amsterdam/design-system-react-icons'; + +export const FullscreenPageFooter = () => { + return ( + <> +
+ + + Colofon + + + + + + Contact + + + Heeft u een vraag en kunt u het antwoord niet vinden op deze + site? Neem dan contact met ons op. + + + + Contactformulier + + + Adressen en openingstijden + + + Bel 14 020 + + + + + + + + Volg de gemeente + + + + Nieuwsbrief Amsterdam + + + Twitter + + + Facebook + + + Instagram + + + LinkedIn + + + Mastodon + + + YouTube + + + Werkenbij + + + + + + + + + Kalender + + + Van buurtactiviteiten tot inspraakavonden. Wat organiseert + de gemeente voor u? Kijk op{' '} + + Kalender Amsterdam + + . + + + + + Uit in Amsterdam + + + Benieuwd wat er allemaal te doen is in de stad? Op{' '} + + Iamsterdam.com + {' '} + vindt u de beste tips op het gebied van cultuur, uitgaan en + evenementen. + + + + + + +
+ + + + Home + Zoeken + Nieuws + Burgerzaken + Kunst en cultuur + Projecten + Project + Parkeren + + + + + ); +}; diff --git a/src/pages/Fullscreen/FullscreenPageHeader.tsx b/src/pages/Fullscreen/FullscreenPageHeader.tsx new file mode 100644 index 0000000..bb40b89 --- /dev/null +++ b/src/pages/Fullscreen/FullscreenPageHeader.tsx @@ -0,0 +1,13 @@ +import { Grid, Header } from '@amsterdam/design-system-react'; + +export const FullscreenPageHeader = () => { + return ( + + +
Menu} + /> + + + ); +}; diff --git a/src/pages/Fullscreen/FullscreenPageMap.tsx b/src/pages/Fullscreen/FullscreenPageMap.tsx new file mode 100644 index 0000000..3a7b1a0 --- /dev/null +++ b/src/pages/Fullscreen/FullscreenPageMap.tsx @@ -0,0 +1,19 @@ +import { AspectRatio } from '@amsterdam/design-system-react'; +import Map from '../../components/Map/Map'; +import ZoomControl from '../../components/ZoomControl/ZoomControl'; + +export type FullScreenPageMapProps = { + scrollWheelZoom?: boolean; +}; + +const FullscreenPageMap = ({ scrollWheelZoom }: FullScreenPageMapProps) => { + return ( + + + + + + ); +}; + +export default FullscreenPageMap; diff --git a/src/pages/Fullscreen/index.ts b/src/pages/Fullscreen/index.ts new file mode 100644 index 0000000..57cf3cb --- /dev/null +++ b/src/pages/Fullscreen/index.ts @@ -0,0 +1,3 @@ +export { FullscreenPage } from './FullscreenPage'; +export { FullscreenPageHeader } from './FullscreenPageHeader'; +export { FullscreenPageFooter } from './FullscreenPageFooter'; diff --git a/src/pages/WFSLayer/WFSLayer.test.tsx b/src/pages/WFSLayer/WFSLayer.test.tsx new file mode 100644 index 0000000..93de7d6 --- /dev/null +++ b/src/pages/WFSLayer/WFSLayer.test.tsx @@ -0,0 +1,35 @@ +import { render } from '@testing-library/react'; +import WFSLayer from './WFSLayer'; +import useGeoJSONLayer from './useGeoJSONLayer'; +import useLeafletMap from './useLeafletMap'; + +vi.mock('./useGeoJSONLayer', () => ({ + __esModule: true, + default: vi.fn(), +})); + +vi.mock('./useLeafletMap', () => ({ + __esModule: true, + default: vi.fn(), +})); + +describe('WFSLayer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders the component', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('calls useLeafletMap', () => { + render(); + expect(useGeoJSONLayer).toHaveBeenCalled(); + }); + + it('calls useGeoJSONLayer', () => { + render(); + expect(useLeafletMap).toHaveBeenCalled(); + }); +}); diff --git a/src/pages/WFSLayer/WFSLayer.tsx b/src/pages/WFSLayer/WFSLayer.tsx new file mode 100644 index 0000000..dcf14a6 --- /dev/null +++ b/src/pages/WFSLayer/WFSLayer.tsx @@ -0,0 +1,21 @@ +import { useRef } from 'react'; +import type { FunctionComponent } from 'react'; +import 'leaflet/dist/leaflet.css'; + +import styles from './styles.module.css'; +import useGeoJSONLayer from './useGeoJSONLayer'; +import useLeafletMap from './useLeafletMap'; + +const WFSLayer: FunctionComponent = () => { + const containerRef = useRef(null); + const mapInstance = useLeafletMap(containerRef); + + useGeoJSONLayer( + mapInstance, + 'https://map.data.amsterdam.nl/maps/bag?REQUEST=Getfeature&VERSION=1.1.0&SERVICE=wfs&TYPENAME=ms:pand&srsName=EPSG:4326&outputformat=geojson&bbox=4.886568897250246%2C52.36966606270195%2C4.892099548064893%2C52.37253554766886' + ); + + return
; +}; + +export default WFSLayer; diff --git a/src/pages/WFSLayer/styles.module.css b/src/pages/WFSLayer/styles.module.css new file mode 100644 index 0000000..d171d7c --- /dev/null +++ b/src/pages/WFSLayer/styles.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 100%; +} diff --git a/src/pages/WFSLayer/useGeoJSONLayer.test.tsx b/src/pages/WFSLayer/useGeoJSONLayer.test.tsx new file mode 100644 index 0000000..b37cd88 --- /dev/null +++ b/src/pages/WFSLayer/useGeoJSONLayer.test.tsx @@ -0,0 +1,54 @@ +import { render } from '@testing-library/react'; +import L from 'leaflet'; +import useGeoJSONLayer from './useGeoJSONLayer'; + +vi.mock('leaflet', () => ({ + __esModule: true, + default: { + map: () => ({ setView: () => ({}) }), + tileLayer: () => ({ addTo: () => ({}) }), + geoJSON: () => ({ addTo: () => ({}) }), + }, +})); + +const mockedGeoJsonResponse = () => + Promise.resolve({ + json() { + return { data: 'mocked data' }; + }, + ok: true, + }) as unknown as Response; + +describe('useGeoJSONLayer', () => { + let originalFetch: typeof global.fetch; + + beforeAll(() => { + originalFetch = global.fetch; + }); + + beforeEach(() => { + // @ts-expect-error mock fetch + global.fetch = vi.fn(mockedGeoJsonResponse); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + it('fetches GeoJSON data and adds it to the map', async () => { + // @ts-expect-error mock fetch + global.fetch = vi.fn(mockedGeoJsonResponse); + + // Test component that uses the hook + const TestComponent = () => { + const map = L.map(document.createElement('div')); + useGeoJSONLayer(map, 'http://example.com/geojson'); + + return
Test component
; + }; + + render(); + + expect(global.fetch).toHaveBeenCalledWith('http://example.com/geojson'); + }); +}); diff --git a/src/pages/WFSLayer/useGeoJSONLayer.ts b/src/pages/WFSLayer/useGeoJSONLayer.ts new file mode 100644 index 0000000..669e5b0 --- /dev/null +++ b/src/pages/WFSLayer/useGeoJSONLayer.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; +import L from 'leaflet'; + +const useGeoJSONLayer = (map: L.Map | null, url: string) => { + useEffect(() => { + if (map === null) { + return; + } + + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + L.geoJSON(data).addTo(map); + }) + .catch(error => { + console.error('Error fetching GeoJSON data:', error); + }); + }, [map, url]); +}; + +export default useGeoJSONLayer; diff --git a/src/pages/WFSLayer/useLeafletMap.test.tsx b/src/pages/WFSLayer/useLeafletMap.test.tsx new file mode 100644 index 0000000..b527242 --- /dev/null +++ b/src/pages/WFSLayer/useLeafletMap.test.tsx @@ -0,0 +1,26 @@ +import { useRef } from 'react'; +import { render, screen } from '@testing-library/react'; +import useLeafletMap from './useLeafletMap'; + +describe('useLeafletMap', () => { + it('initializes a Leaflet map', async () => { + // Test component that uses the hook + const TestComponent = () => { + const containerRef = useRef(null); + const mapInstance = useLeafletMap(containerRef); + + return ( +
+ {mapInstance ? 'Map initialized' : 'Map not initialized'} +
+ ); + }; + + render(); + + // Wait for the map to be initialized + const mapElement = await screen.findByText('Map initialized'); + + expect(mapElement).toBeInTheDocument(); + }); +}); diff --git a/src/pages/WFSLayer/useLeafletMap.ts b/src/pages/WFSLayer/useLeafletMap.ts new file mode 100644 index 0000000..8bf43b3 --- /dev/null +++ b/src/pages/WFSLayer/useLeafletMap.ts @@ -0,0 +1,48 @@ +import { useEffect, useRef, useState } from 'react'; +import type { RefObject } from 'react'; +import L from 'leaflet'; +import getCrsRd from '@/utils/getCrsRd'; + +const useLeafletMap = (container: RefObject) => { + const [mapInstance, setMapInstance] = useState(null); + const createdMapInstance = useRef(false); + + useEffect(() => { + if (container.current === null || createdMapInstance.current !== false) { + return; + } + + const map = new L.Map(container.current, { + center: L.latLng([52.370216, 4.895168]), + zoom: 12, + layers: [ + L.tileLayer('https://{s}.data.amsterdam.nl/topo_rd/{z}/{x}/{y}.png', { + attribution: '', + subdomains: ['t1', 't2', 't3', 't4'], + tms: true, + }), + ], + zoomControl: false, + maxZoom: 16, + minZoom: 3, + crs: getCrsRd(), + maxBounds: [ + [52.36966606270195, 4.886568897250246], + [52.37253554766886, 4.892099548064893], + ], + }); + + map.attributionControl.setPrefix(false); + + createdMapInstance.current = true; + setMapInstance(map); + + return () => { + if (mapInstance) mapInstance.remove(); + }; + }, []); + + return mapInstance; +}; + +export default useLeafletMap; diff --git a/src/stories/Alternatives.mdx b/src/stories/Alternatives.mdx index 4b291a3..973e454 100644 --- a/src/stories/Alternatives.mdx +++ b/src/stories/Alternatives.mdx @@ -1,4 +1,4 @@ -import { Meta } from "@storybook/blocks"; +import { Meta } from '@storybook/blocks'; @@ -9,6 +9,7 @@ import { Meta } from "@storybook/blocks"; ## Map engines #### [ESRI ArcGIS](https://www.esri.com/en-us/arcgis/about-arcgis/overview) + - **Pro:** Provides a seamless end-to-end solution from data-entry to configuring map applications/pages. - **Pro:** Professional and shareable maps. For example, [NYC OpenData](https://data.cityofnewyork.us/Housing-Development/arcGIS/pq44-rrf3) and [Gemeente Eindhoven](https://eindhoven.maps.arcgis.com/home/index.html). - **Pro:** More tools for non-developers @@ -17,12 +18,14 @@ import { Meta } from "@storybook/blocks"; - **Con:** Overwhelming with the number of features #### [Google Maps](https://google.com/maps) + - **Pro:** Familiar UI, delivers fast and smooth UX - **Pro:** Lots of data and frequently refreshed - **Con:** Pricing is a bit complicated - **Con:** Google/USA = legally complicated #### [Leaflet](https://leafletjs.com/) + - **Pro:** Open-source and free to use - **Pro:** Flexible and compatible with multiple providers - **Pro:** Lightweight and minimal code for simple maps @@ -31,6 +34,7 @@ import { Meta } from "@storybook/blocks"; - **Con:** Customizing map styles requires expertise #### [Mapbox](https://mapbox.com/) + - **Pro:** WebGL, fast and nice - **Pro:** Easily customizable (albeit a bit overwhelming) - **Pro:** (Excluding google maps) In the last five years has become the most popular maps packge [(npmtrends)](https://npmtrends.com/@tomtom-international/web-sdk-maps-vs-leaflet-vs-mapbox-gl-vs-maplibre-gl-vs-openlayers) @@ -39,6 +43,7 @@ import { Meta } from "@storybook/blocks"; - **Con:** Can get quickly overcomplicated #### [MapLibre](https://maplibre.org/) + - **Pro:** Derived from OpenLayers; uses WebGL, fast and nice - **Pro:** Open-source and free to use - **Pro:** Flexible and compatible with multiple providers @@ -46,6 +51,7 @@ import { Meta } from "@storybook/blocks"; - **Con:** (Excluding google maps) Is the third most popular map library [(npmtrends)](https://npmtrends.com/@tomtom-international/web-sdk-maps-vs-leaflet-vs-mapbox-gl-vs-maplibre-gl-vs-openlayers) but with less than half the number of users to Mapbox/Leaflet #### [OpenLayers](https://openlayers.org/) + - **Pro:** Open-source and free to use - **Pro:** Flexible and compatible with multiple providers - **Pro:** Large number of [examples](https://openlayers.org/en/latest/examples/) @@ -53,6 +59,7 @@ import { Meta } from "@storybook/blocks"; - **Con:** Not very popular in relation to Leaflet, Mapbox and MapLibre ([(npmtrends)](https://npmtrends.com/@tomtom-international/web-sdk-maps-vs-leaflet-vs-mapbox-gl-vs-maplibre-gl-vs-openlayers)) #### [TomTom Maps](https://www.tomtom.com/products/maps/) + - **Pro:** High quality map data - **Pro:** Fast and smooth UX - **Pro:** More tools for non-developers @@ -65,6 +72,7 @@ import { Meta } from "@storybook/blocks"; #### [Amsterdam Base Layer](../?path=/docs/alternatives--docs) #### [NL Maps](https://nlmaps.nl/) + - **Pro:** Powered and supported by [Kadaster](https://kadaster.nl/) - **Pro:** Nice UI with several themes - **Con:** Setup with Node.js/Typescript is a bit legacy @@ -72,9 +80,10 @@ import { Meta } from "@storybook/blocks"; - **Con:** Dependency on external company if changes are ever needed #### [OpenStreetMap](https://www.openstreetmap.org/) + - **Pro:** Open-source and built on a collaborative community - **Pro:** Large community of contributors - **Con:** Default styling is ugly - **Con:** Dependency on open source contributions if changes are ever needed -Leaflet provide more examples of other tile layer providers [here](https://leaflet-extras.github.io/leaflet-providers/preview/). \ No newline at end of file +Leaflet provide more examples of other tile layer providers [here](https://leaflet-extras.github.io/leaflet-providers/preview/). diff --git a/src/stories/pages/Fullscreen/index.mdx b/src/stories/pages/Fullscreen/index.mdx new file mode 100644 index 0000000..931699b --- /dev/null +++ b/src/stories/pages/Fullscreen/index.mdx @@ -0,0 +1,20 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as FullscreenPageStories from './index.stories'; + + + +# Amsterdam Fullscreen Page Demo + +## Description + +The container for the fullscreen page is set up like this: + +```jsx + + + + + +``` + +The map component is a wrapper around the Leaflet map library and sets a context for child components. The `mapOptions` prop is passed to the Leaflet map instance. diff --git a/src/stories/pages/Fullscreen/index.stories.tsx b/src/stories/pages/Fullscreen/index.stories.tsx new file mode 100644 index 0000000..89d8b7e --- /dev/null +++ b/src/stories/pages/Fullscreen/index.stories.tsx @@ -0,0 +1,34 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { Meta, StoryObj } from '@storybook/react'; +import { + FullscreenPage, + FullscreenPageFooter, + FullscreenPageHeader, +} from '../../../pages/Fullscreen'; +import FullscreenPageMap from '../../../pages/Fullscreen/FullscreenPageMap'; + +const meta = { + title: 'Patterns/Fullscreen', + component: FullscreenPage, + parameters: { + layout: 'fullscreen', + }, + args: { + header: , + map: , + footer: , + }, + argTypes: { + header: { control: { disable: true } }, + map: { control: { disable: true } }, + footer: { control: { disable: true } }, + }, +} satisfies Meta; + +export default meta; + +export const Default: StoryObj = {}; diff --git a/src/stories/pages/WFSLayer/index.mdx b/src/stories/pages/WFSLayer/index.mdx new file mode 100644 index 0000000..d9e47aa --- /dev/null +++ b/src/stories/pages/WFSLayer/index.mdx @@ -0,0 +1,65 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as WFSLayerStories from './index.stories'; + +import WFSLayer from '@/pages/WFSLayer/WFSLayer?raw'; +import styles from '@/pages/WFSLayer/styles.module.css?raw'; +import getCrsRd from '@/utils/getCrsRd?raw'; +import useGeoJSONLayer from '@/pages/WFSLayer/useGeoJSONLayer?raw'; +import useLeafletMap from '@/pages/WFSLayer/useLeafletMap?raw'; + + + +# Amsterdam WFSLayer + +## Requirements + +- [BaseLayer](../?path=/docs/react-baselayer--docs) +- GeoJson data endpoint + +## Description + +This is the Amsterdam WFS layer on a plain Leaflet map. + +## Background + +### WFS layer + +A WFS (Web Feature Service) layer is a layer that is served as a GeoJson file. This file can be requested by a URL. The URL can be used to fetch the GeoJson data and display it on a map. + +## How to implement + +To accomplish the Amsterdam WFS layer there are four files: + +1. The React components + - [WFSLayer.tsx](#1-wfslayertsx) +2. The custom hooks (2 files) + - [useGeoJsonLayer.ts](#1-usegeojsonlayerts) + - [useLeafletMap.ts](#1-useleafletmapts) +3. The CSS styles (1 file) + - [styles.module.css](#1-stylesmodulecss) + +## Usage + +The following files are required: + +### React Components + +#### 1. WFSLayer.tsx + + + +### Custom Hooks + +#### 1. useGeoJsonLayer.ts + + + +#### 2. useLeafletMap.ts + + + +### CSS Styling + +#### 1. styles.module.css + + diff --git a/src/stories/pages/WFSLayer/index.stories.ts b/src/stories/pages/WFSLayer/index.stories.ts new file mode 100644 index 0000000..a7a5bd5 --- /dev/null +++ b/src/stories/pages/WFSLayer/index.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import WFSLayer from '@/pages/WFSLayer/WFSLayer'; + +const meta = { + title: 'React/WFSLayer', + component: WFSLayer, + parameters: { + layout: 'fullscreen', + options: { + panelPosition: 'bottom', + bottomPanelHeight: 0, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base: Story = {}; diff --git a/src/stories/pages/ZoomControl/index.mdx b/src/stories/pages/ZoomControl/index.mdx new file mode 100644 index 0000000..8c1e6bd --- /dev/null +++ b/src/stories/pages/ZoomControl/index.mdx @@ -0,0 +1,40 @@ +import { Canvas, Meta, Source, Story } from '@storybook/blocks'; +import * as ZoomControlStories from './index.stories'; +import ZoomControl from '@/components/ZoomControl/ZoomControl?raw'; +import styles from '@/components/ZoomControl/styles.module.css?raw'; + + + +# Amsterdam ZoomControl + +# Description + +The container for the zoom control. + +## Usage + +#### ZoomControl with Design System buttons + + + +### CSS Styling + +#### 1. styles.module.css + + + +### JavaScript + +```js +const handleZoomInClick = () => { + if (mapInstance) { + mapInstance?.setZoom(mapInstance.getZoom() + 1); + } +}; + +const handleZoomOutClick = () => { + if (mapInstance) { + mapInstance?.setZoom(mapInstance.getZoom() - 1); + } +}; +``` diff --git a/src/stories/pages/ZoomControl/index.stories.tsx b/src/stories/pages/ZoomControl/index.stories.tsx new file mode 100644 index 0000000..05071c3 --- /dev/null +++ b/src/stories/pages/ZoomControl/index.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ZoomControl from '@/components/ZoomControl/ZoomControl'; +import { Story } from '@storybook/blocks'; +import Map from '../../../components/Map/Map'; + +const meta = { + title: 'Components/ZoomControl', + component: ZoomControl, + decorators: [ + Story => ( + + + + ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {};