diff --git a/docs/changelog.md b/docs/changelog.md index a89902100..9f61c8381 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,31 @@ # Changelog +3.7.0 (2022-03-11) +------------------ + +**🚀 New features** + +* Include 3D view in trek detail pages (#390) +* Add ``maxLengthTrekAllowedFor3DRando`` setting to define the maximum length of a trek to enable the 3D view on its page (or to disable 3D view) (#390) +* Add breadcrumb on detail and information pages (#506) +* Add Open System reservation widget on trek detail pages with an ``id_reservation`` (#382) +* Add ``reservationPartner`` and ``reservationProject`` settings to enable Open System reservation widget (#382) +* Add a customizable color for each content category (treks, outdoor sites, services and events) to better differentiate content categories (#437) +* Add content type name display on hover of pictograms in search page (#437) +* Add the possibility to define outdoor sites, services and events suggestions on home page (#372) +* Display more information on suggestions cards on home page (#372) +* Improve layer controller on treks and outdoor sites detail pages (#449) +* Add a panel on trek mobile map to display trek title, steps and altimetric profile (#452) +* Display SVG theme pictograms in filters +* Improve modale component +* Add German, Spanish and Catalan translations (#571) + +**🐛 Fixes** + +* Fix categories display in mobile filters depending on contents and settings (#586) +* Fix trek filters displayed in 3 columns (#377) +* Display ``disabledInfrastructure`` and ``accessibilities`` in trek detail pages only if they have content + 3.6.0 (2022-02-07) ------------------ diff --git a/docs/customization.md b/docs/customization.md index ef503ab2b..1b0d60f89 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -34,10 +34,11 @@ In json files, you can just override the primary keys you need. You have to over - `applicationName`: application name appearing on PWA - `enableReport`: to enable report form in trek detail pages - `enableSearchByMap`: to enable searching by map displayed area (bbox) + - `maxLengthTrekAllowedFor3DRando`: Maximum length of meters allowed to enable 3D mode in the current trek. Adjust this setting carefully as too long a trek could freeze your browser. If this setting is defined to `0` (or `mapSatelliteLayerUrl` from `map.json` is not set) the 3D mode feature is disabled for the whole application - `header.json` to define logo URL, default and available languages, number items to flatpages to display in navbar (see default values in https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/config/header.json) -- `home.json` to define homepage settings. You can define blocks to display and trek suggestion block with trek ID to highlight on homepage (see https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/home.json). +- `home.json` to define homepage settings. You can define blocks to display and trek suggestion block with treks ID, outdoor sites ID, services ID or events ID to highlight on homepage (see https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/home.json). - In `welcomeBanner`, you can personnalize the cover on the homepage. You can add an asset on the top of the page: it can either be a video, a single picture or a carousel of images: @@ -120,6 +121,19 @@ Example for Cevennes national park orange colors: } ``` +It's also possible to change category colors : + +```json +{ + "categories": { + "trek": "blue", + "events": "red", + "outdoor": "#62AB41", + "service": "#3B89A2" + } +} +``` + You can also override CSS in `customization/theme/style.css` file. To help overriding CSS, some ID have been added on main DIV components (header, logo, footer, cover, cards, results, maps, details...). ## Translations diff --git a/docs/icons.md b/docs/icons.md index 3424a679e..560c7ef96 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -1,6 +1,6 @@ # Icons -Geotrek V3 uses icons which images come from API. This file intends to help upload a right format for them in Geotrek Admin. +Geotrek-rando V3 uses icons which images come from API. This file intends to help upload a right format for them in Geotrek-admin. ## Activity icons @@ -10,15 +10,17 @@ Because of that, they have to be : - in **`svg` format** - **white**, or with a light color, by default (a color that is visible on a `primary1` background) - **without filled background** : only shapes or lines +- without `width` and `height` attributes - preferrably, **no different colors**. If different colors are used, they will be lost anyway when the icon is modified. The icon has to stay readable with all its shapes and lines in one color. +- with an usable licence and credits -**Default activity icons**, not modified, are visible in search filter list or on map : +**Default activity icons**, not modified, are visible in search filter list or on map ![Activity Icon on filter list](assets/iconFilterList.png) -Here that "Alpinisme" icon should have been white by default. +Here the "Alpinisme" icon should have been white by default. -**Modified activity icons** are visible on activity bar on home or details page : +**Modified activity icons** are visible on activity bar on home or details page: ![Activity Icon modified on home](assets/iconHome.png) diff --git a/frontend/config/global.json b/frontend/config/global.json index f8808e6a5..539bf6268 100644 --- a/frontend/config/global.json +++ b/frontend/config/global.json @@ -8,6 +8,8 @@ "enableTouristicEvents": true, "portalIds": [], "apiUrl": "https://geotrekdemo.ecrins-parcnational.fr/api/v2", + "reservationPartner": null, + "reservationProject": null, "googleAnalyticsId": null, "googleSiteVerificationToken": "eKAyxwaXAobFWQcJen0mnZ8T3CpLoN45JysXeNkRf38", "baseUrl": "http://127.0.0.1:3000", @@ -17,5 +19,7 @@ "enableIndexation": true, "enableReport": true, "enableSearchByMap": true, - "enableServerCache": true -} + "enableServerCache": true, + "enableMeteoWidget": true, + "maxLengthTrekAllowedFor3DRando": 25000 +} \ No newline at end of file diff --git a/frontend/customization/config/global.json b/frontend/customization/config/global.json index ca7296fd0..cd93b0283 100644 --- a/frontend/customization/config/global.json +++ b/frontend/customization/config/global.json @@ -8,6 +8,8 @@ "enableTouristicEvents": true, "portalIds": [1], "apiUrl": "https://geotrekdemo.ecrins-parcnational.fr/api/v2", + "reservationPartner": "parcdesecrins", + "reservationProject": "parcdesecrins", "googleAnalyticsId": "G-8FSV2N4FXN", "googleSiteVerificationToken": "eKAyxwaXAobFWQcJen0mnZ8T3CpLoN45JysXeNkRf38", "baseUrl": "http://localhost:3000", diff --git a/frontend/customization/config/home.json b/frontend/customization/config/home.json index 33904cab8..90a8addf2 100644 --- a/frontend/customization/config/home.json +++ b/frontend/customization/config/home.json @@ -14,12 +14,31 @@ { "titleTranslationId": "home.territoryTreks", "iconUrl": "/icons/practice-pedestrian.svg", - "ids": ["2", "582", "586", "501", "771", "596", "604"] + "ids": ["2", "582", "586", "501", "771", "596"], + "type": "trek" + }, + { + "titleTranslationId": "home.events", + "iconUrl": "/icons/category-events.svg", + "ids": ["1", "5"], + "type": "events" + }, + { + "titleTranslationId": "home.outdoor", + "iconUrl": "/icons/practice-outdoor.svg", + "ids": ["65", "12", "79"], + "type": "outdoor" + }, + { + "titleTranslationId": "home.services", + "iconUrl": "/icons/category-services.svg", + "ids": ["1", "6", "476"], + "type": "service" }, { "titleTranslationId": "home.itinerantTreks", "iconUrl": "/icons/practice-pedestrian.svg", - "ids": ["501", "771"] + "ids": ["501", "771", "604"] } ] } diff --git a/frontend/next.config.js b/frontend/next.config.js index ee5c7de90..73be56d84 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -24,6 +24,20 @@ module.exports = withPlugins(plugins, { webpack(config) { config.resolve.modules.push(path.resolve('./src')); + Object.assign(config.resolve.alias, { + // GSAP aliases are useful for @makina-corpus/rando3d package + EasePack: 'gsap/src/uncompressed/easing/EasePack.js', + TweenLite: 'gsap/src/uncompressed/TweenLite.js', + TimelineLite: 'gsap/src/uncompressed/TimelineLite.js', + BezierPlugin: 'gsap/src/uncompressed/plugins/BezierPlugin.js', + DirectionalRotationPlugin: 'gsap/src/uncompressed/plugins/DirectionalRotationPlugin.js', + }); + + config.module.rules.push({ + test: /\.html$/, + use: 'raw-loader', + }); + return config; }, pwa: { diff --git a/frontend/package.json b/frontend/package.json index d8a658b39..7593fdca1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "geotrek-rando-frontend", - "version": "3.6.0", + "version": "3.7.0", "private": true, "scripts": { "debug": "NODE_OPTIONS='--inspect' next ./src", @@ -33,6 +33,7 @@ }, "dependencies": { "@fullhuman/postcss-purgecss": "^4.0.3", + "@makina-corpus/rando3d": "^1.3.1", "@next/bundle-analyzer": "11.1.2", "@raruto/leaflet-elevation": "1.7.0", "@sentry/browser": "6.13.2", @@ -128,12 +129,14 @@ "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "4.12.3", "full-icu": "1.3.4", + "gsap": "1.16.1", "html-loader-jest": "^0.2.1", "isomorphic-fetch": "^3.0.0", "jest": "27.2.1", "next-server": "^9.0.5", "nock": "13.1.3", "prettier": "2.4.1", + "raw-loader": "^4.0.2", "react-addons-test-utils": "^15.6.2", "stylelint": "13.13.1", "stylelint-config-prettier": "^8.0.2", diff --git a/frontend/src/components/3D/3D.style.ts b/frontend/src/components/3D/3D.style.ts new file mode 100644 index 000000000..9cc9c67a4 --- /dev/null +++ b/frontend/src/components/3D/3D.style.ts @@ -0,0 +1,62 @@ +import styled from 'styled-components'; +import { colorPalette } from 'stylesheet'; + +export const Wrapper = styled.div` + width: 90vw; + height: 90vh; + background-color: ${colorPalette.greyDarkColored}; + background-image: url(/images/3d/background.jpg); + background-size: 100% 100%; +`; +export const Control = styled.div` + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0 0 0.35em 0.35em; +`; + +export const Camera = styled.ul` + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.35em 0 0 0.35em; +`; + +export const CameraItem = styled.li` + border-left: 1px solid transparent; + border-bottom: solid 1px rgba(255, 255, 255, 0.2); + &.camera--disabled { + cursor: default; + opacity: 0.4; + } + &.camera--selected { + cursor: auto; + border-left-color: white; + } + &:last-child { + border-bottom: none; + } +`; + +export const LoaderOverlay = styled.div` + background-color: rgba(255, 255, 255, 0.2); +`; + +export const PoiSide = styled.div` + background-color: rgba(20, 20, 20, 0.8); + top: 0; + left: -100%; + width: 24rem; + height: 100%; + transition: left 0.3s ease-out; + &.opened { + left: 0; + } +`; + +export const Poi = styled.div` + background-color: rgba(255, 255, 255, 0.2); + position: absolute; + width: auto; + height: auto; + padding: 2px; + border-radius: 0.35em; + text-align: center; + display: none; +`; diff --git a/frontend/src/components/3D/Interface.tsx b/frontend/src/components/3D/Interface.tsx new file mode 100644 index 000000000..6e7d43e48 --- /dev/null +++ b/frontend/src/components/3D/Interface.tsx @@ -0,0 +1,115 @@ +import Image from 'next/image'; +import { MessageFormatElement, useIntl } from 'react-intl'; +import { Camera, CameraItem, Control } from './3D.style'; + +const getControls = (t: Record | Record) => [ + { + control: 'examine', + title: t['rando3D.views.examine.title'], + steps: [ + { + label: t['rando3D.instructions.zoom'], + action: {t['rando3D.actions.scrollMouse']}, + }, + { + label: t['rando3D.instructions.lookAround'], + action: {t['rando3D.actions.leftClick']}, + }, + { + label: t['rando3D.instructions.moveAround'], + action: , + }, + ], + cameraTitle: t['rando3D.views.examine.cameraTitle'], + }, + { + control: 'bird', + title: t['rando3D.views.bird.title'], + steps: [ + { + label: t['rando3D.instructions.zoom'], + action: {t['rando3D.actions.scrollMouse']}, + }, + { + label: t['rando3D.instructions.lookAround'], + action: {t['rando3D.actions.leftClick']}, + }, + { + label: t['rando3D.instructions.moveAround'], + action: , + }, + ], + cameraTitle: t['rando3D.views.bird.cameraTitle'], + }, + { + control: 'hiker', + title: t['rando3D.views.hiker.title'], + steps: [ + { + label: t['rando3D.instructions.lookAround'], + action: {t['rando3D.actions.leftClick']}, + }, + { + label: t['rando3D.instructions.playPause'], + action: {t['rando3D.actions.space']}, + }, + { + label: t['rando3D.instructions.stop'], + action: {t['rando3D.actions.enter']}, + }, + ], + cameraTitle: t['rando3D.views.examine.hiker'], + }, +]; + +const Interface: React.FC = () => { + const { messages } = useIntl(); + const controls = getControls(messages); + return ( +
+ {controls.map(({ control, title, steps }) => ( + +

{title}

+

+ {steps.map(({ label, action }, index) => ( + + {label} {action} + + ))} + + ))} + + + {controls.map(({ control, cameraTitle }) => ( + + + + ))} + + +

+ + + +
+ + ©IGN +
+ ); +}; + +export default Interface; diff --git a/frontend/src/components/3D/index.tsx b/frontend/src/components/3D/index.tsx new file mode 100644 index 000000000..cf5d7b37f --- /dev/null +++ b/frontend/src/components/3D/index.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from 'react'; +import Popup from 'components/Popup'; +import { getGlobalConfig } from 'modules/utils/api.config'; +import { getMapConfig } from 'components/Map/config'; +import Loader from 'components/Loader'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Cross } from 'components/Icons/Cross'; +import { useRouter } from 'next/router'; +import { getDefaultLanguage } from 'modules/header/utills'; +import { DetailsAdvice } from 'components/pages/details/components/DetailsAdvice'; +import { LoaderOverlay, Poi, PoiSide, Wrapper } from './3D.style'; +import Interface from './Interface'; + +declare global { + interface Window { + jQuery?: any; + Rando3D?: any; + } +} + +interface ThreeDProps { + demURL: string; + profileURL: string; + onRequestClose: () => void | undefined; + title: string; + trekId: number; +} + +interface SceneProps { + deinit: () => void; + init: (callback: () => void) => void; +} + +export const ThreeD: React.FC = ({ + demURL, + profileURL, + onRequestClose, + title, + trekId, +}) => { + const canvasRef = useRef(null); + const [isLoading, setLoading] = useState(true); + const [libLoaded, setLibLoaded] = useState(false); + const scene = useRef(null); + const { messages } = useIntl(); + + const router = useRouter(); + const currentLanguage = router.locale ?? getDefaultLanguage(); + + const isAvailableWebGL = 'WebGLRenderingContext' in window; + + const handleClose = () => { + if (scene.current) { + scene.current.deinit(); + scene.current = null; + } + onRequestClose(); + }; + + useEffect(() => { + if (window === undefined) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + return; + } + + // Dynamicly loads jQuery and Rando3D + async function loadLibraries() { + // @ts-ignore next-line + const { default: jQuery } = await import('jquery'); + // @ts-ignore next-line + await import('@makina-corpus/rando3d/dist/rando3D'); + window.jQuery = jQuery; + setLibLoaded(true); + } + + void loadLibraries(); + }, []); + + useEffect(() => { + if ( + window?.jQuery === undefined || + window?.Rando3D === undefined || + !libLoaded || + !isAvailableWebGL + ) { + return; + } + + const customSettings = { + IMAGES_FOLDER: '/images/3d/', + DEM_URL: demURL, + PROFILE_URL: profileURL, + POI_URL: `${ + getGlobalConfig().apiUrl + }/poi/?trek=${trekId}&language=${currentLanguage}&format=geojson`, + TILE_TEX_URL: getMapConfig().mapSatelliteLayerUrl, + SIDE_TEX_URL: '/images/3d/side.jpg', + CAM_SPEED_F: 100, + PICTO_PREFIX: '', + TREK_COLOR: { + R: 0.6, + V: 0.1, + B: 0.1, + }, + NO_DESCRIPTION_MESSAGE: messages['rando3D.poi.no-descriptions'], + CAMERA_MESSAGES: { + bird: messages['rando3D.views.bird.description'], + examine: messages['rando3D.views.examine.description'], + hiker: messages['rando3D.views.hiker.description'], + }, + }; + + if (scene.current !== null) { + scene.current.deinit(); + scene.current = null; + } + + const app3D = new window.Rando3D(); + scene.current = app3D.init(customSettings, canvasRef.current, 'examine'); + scene.current && scene.current.init(() => setLoading(false)); + }, [currentLanguage, demURL, profileURL, libLoaded]); + + const noWebGL = messages['rando3D.warning.noWebGl'] as string; + + return ( + + {!isAvailableWebGL && } + {isAvailableWebGL && ( + + {isLoading && ( + + + + )} + + + + +

+
+ + + + + )} + + ); +}; diff --git a/frontend/src/components/CardIcon/CardIcon.tsx b/frontend/src/components/CardIcon/CardIcon.tsx index 48a4bfb9f..e7fabd5d2 100644 --- a/frontend/src/components/CardIcon/CardIcon.tsx +++ b/frontend/src/components/CardIcon/CardIcon.tsx @@ -1,19 +1,86 @@ import { colorPalette, fillSvgWithColor } from 'stylesheet'; import SVG from 'react-inlinesvg'; +import styled from 'styled-components'; -export const CardIcon: React.FC<{ iconUri: string }> = ({ iconUri }) => { - const classNameContainer = - 'absolute top-4 left-4 h-8 w-8 rounded-full shadow-sm text-white bg-primary1 border-2 border-white border-solid'; +interface IconProps { + iconUri: string; + color?: string; +} +interface Props extends IconProps { + iconName: string; +} + +const Wrapper = styled.div<{ color?: string }>` + z-index: 100; + background: ${props => props.color}; + + & > div { + max-width: 0; + overflow: hidden; + white-space: nowrap; + transition: max-width 0.6s; + } + + &:hover { + & > div { + max-width: 150px; + } + } +`; + +const Label = styled.div` + text-align: center; + flex: auto; + + & > div { + padding: 0 10px; + } +`; + +const StyledSVG = styled(SVG)` + height: 28px; + width: 28px; +`; + +const Img = styled.img` + height: 28px; + width: 28px; +`; + +const NoImg = styled.span<{ color?: string }>` + display: block; + height: 28px; + width: 28px; + border-radius: 50%; + background: ${props => props.color}; +`; + +const Icon: React.FC = ({ iconUri, color }) => { + if (!iconUri) { + return ; + } if (RegExp(/(.*).svg/).test(iconUri)) { return ( -
- -
+ ); } - return ; + return ; +}; + +export const CardIcon: React.FC = ({ iconUri, iconName, color }) => { + return ( + + + + + ); }; diff --git a/frontend/src/components/Icons/ThreeDMap/index.tsx b/frontend/src/components/Icons/ThreeDMap/index.tsx new file mode 100644 index 000000000..a9b0ecd1c --- /dev/null +++ b/frontend/src/components/Icons/ThreeDMap/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { GenericIconProps } from '../types'; + +export const ThreeDMap: React.FC = ({ + color = 'currentColor', + opacity, + className, + size, +}) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/components/Map/DetailsMap/DetailsMap.tsx b/frontend/src/components/Map/DetailsMap/DetailsMap.tsx index ba3de207b..431d46074 100644 --- a/frontend/src/components/Map/DetailsMap/DetailsMap.tsx +++ b/frontend/src/components/Map/DetailsMap/DetailsMap.tsx @@ -3,6 +3,7 @@ import { LatLngBoundsExpression } from 'leaflet'; import React from 'react'; import { MapContainer, TileLayer } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; +import styled from 'styled-components'; import { ArrowLeft } from 'components/Icons/ArrowLeft'; @@ -14,7 +15,7 @@ import { } from 'modules/interface'; import { useTileLayer } from 'hooks/useTileLayer'; import { MapLayerTypeToggleButton } from 'components/MapLayerTypeToggleButton/MapLayerTypeToggleButton'; -import { TrekChildGeometry } from 'modules/details/interface'; +import { TrekChildGeometry, TrekFamily } from 'modules/details/interface'; import { SensitiveAreaGeometry } from 'modules/sensitiveArea/interface'; import { MapButton } from '../components/MapButton'; @@ -25,6 +26,7 @@ import { AltimetricProfile } from '../components/AltimetricProfile'; import { ControlSection } from '../components/ControlSection'; import { useDetailsMap } from './useDetailsMap'; import { MapChildren, PointWithIcon } from './MapChildren'; +import DetailsMapDrawer from '../components/DetailsMapDrawer'; export interface TouristicContentGeometry { geometry: PointGeometry | PolygonGeometry | LineStringGeometry; @@ -50,10 +52,12 @@ export type PropsType = { parkingLocation?: Coordinate2D; shouldUsePopups?: boolean; bbox: { corner1: Coordinate2D; corner2: Coordinate2D }; + trekFamily?: TrekFamily | null; trekChildrenGeometry?: TrekChildGeometry[]; sensitiveAreas?: SensitiveAreaGeometry[]; trekId: number; advisedParking?: string; + title?: string; }; export const DetailsMap: React.FC = props => { @@ -87,7 +91,7 @@ export const DetailsMap: React.FC = props => { return ( <> - = props => { attributionControl={false} whenCreated={setMapInstance} bounds={center} + hasDrawer={!!props.title} > {props.trekGeometry && ( @@ -129,13 +134,25 @@ export const DetailsMap: React.FC = props => { referencePointsMobileVisibility={referencePointsMobileVisibility} touristicContentMobileVisibility={touristicContentMobileVisibility} /> - {props.trekGeoJSON && } + {props.trekGeoJSON && ( + + )} {isSatelliteLayerAvailable && ( -
+
updateTileLayer(newType)} />
)} - + {props.title && ( +
+ +
+ )} + } onClick={hideMap} /> = props => { ); }; +const StyledMapContainer = styled(MapContainer)<{ hasDrawer: boolean }>` + .leaflet-bottom { + margin-bottom: ${props => (props.hasDrawer ? '40px' : 0)}; + } +`; + export default DetailsMap; diff --git a/frontend/src/components/Map/DetailsMap/MarkersWithIcon.tsx b/frontend/src/components/Map/DetailsMap/MarkersWithIcon.tsx index a9b16805c..11c12bda4 100644 --- a/frontend/src/components/Map/DetailsMap/MarkersWithIcon.tsx +++ b/frontend/src/components/Map/DetailsMap/MarkersWithIcon.tsx @@ -18,7 +18,7 @@ export const MarkersWithIcon: React.FC = props => { id={point.id} position={[point.location.y, point.location.x]} pictogramUri={point.pictogramUri} - type="TREK" + type={null} > {point.name} diff --git a/frontend/src/components/Map/DetailsMap/TouristicContent.tsx b/frontend/src/components/Map/DetailsMap/TouristicContent.tsx index 80e039b5b..5119a67c5 100644 --- a/frontend/src/components/Map/DetailsMap/TouristicContent.tsx +++ b/frontend/src/components/Map/DetailsMap/TouristicContent.tsx @@ -26,7 +26,7 @@ export const TouristicContent: React.FC = ({ contents, type = 'TOURIS id={id} position={[geometry.coordinates.y, geometry.coordinates.x]} pictogramUri={pictogramUri} - type="TREK" + type={type} > diff --git a/frontend/src/components/Map/Markers/TrekChildMarker.tsx b/frontend/src/components/Map/Markers/TrekChildMarker.tsx index 0ad7eff72..dc02d3aa9 100644 --- a/frontend/src/components/Map/Markers/TrekChildMarker.tsx +++ b/frontend/src/components/Map/Markers/TrekChildMarker.tsx @@ -19,10 +19,14 @@ const ChildLabel = styled.span<{ zoomRatio: number }>` text-align: center; `; -const ChildMarker: React.FC<{ label: string; zoomRatio: number }> = ({ label, zoomRatio }) => { +const ChildMarker: React.FC<{ label: string; zoomRatio: number; color: string }> = ({ + label, + zoomRatio, + color, +}) => { return (
- + {label} @@ -30,11 +34,13 @@ const ChildMarker: React.FC<{ label: string; zoomRatio: number }> = ({ label, zo ); }; -export const TrekChildMarker = (rank: number, zoomRatio = 1): DivIcon => +export const TrekChildMarker = (rank: number, zoomRatio = 1, color: string): DivIcon => new DivIcon({ iconSize: [markerHeight * zoomRatio, markerWidth * zoomRatio], // point of the icon which will correspond to marker's location iconAnchor: [(markerWidth * zoomRatio) / 2, markerHeight * zoomRatio], // horizontal middle of the icon and bottom of it - html: renderToStaticMarkup(), + html: renderToStaticMarkup( + , + ), className: 'bg-none border-none', }); diff --git a/frontend/src/components/Map/Markers/TrekMarker.tsx b/frontend/src/components/Map/Markers/TrekMarker.tsx index 43362f7ba..e75d8fd52 100644 --- a/frontend/src/components/Map/Markers/TrekMarker.tsx +++ b/frontend/src/components/Map/Markers/TrekMarker.tsx @@ -16,13 +16,14 @@ const ActivityPictogram = styled.img<{ zoomRatio: number }>` top: ${props => markerTopPadding * props.zoomRatio}px; `; -const ActivityMarker: React.FC<{ pictogramUrl?: string; zoomRatio: number }> = ({ +const ActivityMarker: React.FC<{ pictogramUrl?: string; zoomRatio: number; color: string }> = ({ pictogramUrl, zoomRatio, + color, }) => { return (
- + = ( ); }; -export const TrekMarker = (pictogramUrl?: string, zoomRatio = 1) => +export const TrekMarker = (pictogramUrl?: string, zoomRatio = 1, color?: string) => new DivIcon({ iconSize: [markerHeight * zoomRatio, markerWidth * zoomRatio], // point of the icon which will correspond to marker's location iconAnchor: [(markerWidth * zoomRatio) / 2, markerHeight * zoomRatio], // horizontal middle of the icon and bottom of it html: renderToStaticMarkup( - , + , ), className: 'bg-none border-none', }); diff --git a/frontend/src/components/Map/components/AltimetricProfile/index.tsx b/frontend/src/components/Map/components/AltimetricProfile/index.tsx index de804464f..560c0267f 100644 --- a/frontend/src/components/Map/components/AltimetricProfile/index.tsx +++ b/frontend/src/components/Map/components/AltimetricProfile/index.tsx @@ -9,17 +9,16 @@ import { getDefaultLanguage } from 'modules/header/utills'; interface AltimetricProfileProps { trekGeoJSON: string; + id: string; } -const DIV_ID = 'altimetric-profile'; - -export const AltimetricProfile: React.FC = ({ trekGeoJSON }) => { +export const AltimetricProfile: React.FC = ({ trekGeoJSON, id }) => { const map = useMap(); const intl = useIntl(); const language = useRouter().locale ?? getDefaultLanguage(); useEffect(() => { - const div = document.getElementById(DIV_ID); + const div = document.getElementById(id); if (div) div.innerHTML = ''; // @ts-ignore @@ -27,7 +26,7 @@ export const AltimetricProfile: React.FC = ({ trekGeoJSO theme: 'lightblue-theme', collapsed: false, detached: true, - elevationDiv: `#${DIV_ID}`, + elevationDiv: `#${id}`, summary: 'inline', marker: 'position-marker', followMarker: false, diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/Check.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/Check.tsx new file mode 100644 index 000000000..aba0cef3d --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/Check.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/IconAnimal.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconAnimal.tsx new file mode 100644 index 000000000..e23e0f6cf --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconAnimal.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/IconDrapeau.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconDrapeau.tsx new file mode 100644 index 000000000..306184d0b --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconDrapeau.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/IconInfo.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconInfo.tsx new file mode 100644 index 000000000..e23e0f6cf --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconInfo.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/IconLocation.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconLocation.tsx new file mode 100644 index 000000000..6e85d55b6 --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconLocation.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/IconPatrimoine.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconPatrimoine.tsx new file mode 100644 index 000000000..957974224 --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/IconPatrimoine.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/Line.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/Line.tsx new file mode 100644 index 000000000..80647fe1c --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/Line.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import { colorPalette } from 'stylesheet'; +import { useIntl } from 'react-intl'; +import Check from './Check'; +import IconLocation from './IconLocation'; + +const Wrapper = styled.div<{ active: boolean }>` + color: ${props => (props.active ? colorPalette.primary1 : colorPalette.greyDarkColored)}; + + display: flex; + align-items: center; + font-weight: bold; +`; + +const IconWrapper = styled.div` + & svg { + height: 25px; + width: 25px; + margin-right: 10px; + } +`; + +const Text = styled.div` + flex: auto; +`; + +export const Line: React.FC<{ Icon: any; active: boolean; toggle: () => void; transKey: string }> = + ({ Icon, active, toggle, transKey }) => { + const intl = useIntl(); + return ( + + + + + {intl.formatMessage({ id: transKey })} + {} + + ); + }; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx new file mode 100644 index 000000000..b9fd592c2 --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx @@ -0,0 +1,82 @@ +import { Visibility } from 'components/Map/DetailsMap/useDetailsMap'; +import styled from 'styled-components'; +import { Line } from './Line'; +import IconLocation from './IconLocation'; +import IconInfo from './IconInfo'; +import IconDrapeau from './IconDrapeau'; +import IconPatrimoine from './IconPatrimoine'; + +const Wrapper = styled.div` + background: white; + box-shadow: 0px 4px 30px 0px rgba(0, 0, 0, 0.15); + padding: 16px; + border-radius: 16px; + width: 230px; + display: flex; + flex-flow: column; + + & div { + margin-bottom: 8px; + } + + & div:last-child { + margin-bottom: 0; + } +`; + +export const ControlPanel: React.FC<{ + trekChildrenVisibility: Visibility; + toggleTrekChildrenVisibility: () => void; + poiVisibility: Visibility; + togglePoiVisibility: () => void; + referencePointsVisibility: Visibility; + toggleReferencePointsVisibility: () => void; + touristicContentVisibility: Visibility; + toggleTouristicContentVisibility: () => void; +}> = ({ + trekChildrenVisibility, + toggleTrekChildrenVisibility, + poiVisibility, + togglePoiVisibility, + referencePointsVisibility, + toggleReferencePointsVisibility, + touristicContentVisibility, + toggleTouristicContentVisibility, +}) => { + return ( + + {trekChildrenVisibility !== null && ( + + )} + {poiVisibility !== null && ( + + )} + {referencePointsVisibility !== null && ( + + )} + {touristicContentVisibility !== null && ( + + )} + + ); +}; diff --git a/frontend/src/components/Map/components/ControlSection/ControlSection.tsx b/frontend/src/components/Map/components/ControlSection/ControlSection.tsx index db9766535..f3069d388 100644 --- a/frontend/src/components/Map/components/ControlSection/ControlSection.tsx +++ b/frontend/src/components/Map/components/ControlSection/ControlSection.tsx @@ -5,19 +5,28 @@ import { Florist } from 'components/Icons/Florist'; import { Sliders } from 'components/Icons/Sliders'; import { Visibility } from 'components/Map/DetailsMap/useDetailsMap'; import { Point } from 'components/Icons/Point'; +import styled from 'styled-components'; import { ControlButton } from '../ControlButton'; import { useControlSection } from './useControlSection'; +import { ControlPanel } from './ControlPanel'; +import { Layers } from './Layers'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; +`; interface ControlSectionProps { - trekChildrenVisibility?: Visibility; - toggleTrekChildrenVisibility?: () => void; - poiVisibility?: Visibility; - togglePoiVisibility?: () => void; - referencePointsVisibility?: Visibility; + trekChildrenVisibility: Visibility; + toggleTrekChildrenVisibility: () => void; + poiVisibility: Visibility; + togglePoiVisibility: () => void; + referencePointsVisibility: Visibility; toggleReferencePointsVisibility: () => void; - touristicContentVisibility?: Visibility; + touristicContentVisibility: Visibility; toggleTouristicContentVisibility: () => void; - className?: string; + className: string; } export const ControlSection: React.FC = ({ @@ -32,44 +41,27 @@ export const ControlSection: React.FC = ({ className, }) => { const { controlSectionState, expandControlSection, collapseControlSection } = useControlSection(); + return ( -
+ {controlSectionState === 'COLLAPSED' && ( - } onClick={expandControlSection} /> + } onClick={expandControlSection} /> )} {controlSectionState === 'EXPANDED' && ( <> - } onClick={collapseControlSection} /> - {trekChildrenVisibility !== null && ( - } - onClick={toggleTrekChildrenVisibility} - active={trekChildrenVisibility === 'DISPLAYED'} - /> - )} - {poiVisibility !== null && ( - } - onClick={togglePoiVisibility} - active={poiVisibility === 'DISPLAYED'} - /> - )} - {referencePointsVisibility !== null && ( - } - onClick={toggleReferencePointsVisibility} - active={referencePointsVisibility === 'DISPLAYED'} - /> - )} - {touristicContentVisibility !== null && ( - } - onClick={toggleTouristicContentVisibility} - active={touristicContentVisibility === 'DISPLAYED'} - /> - )} + } onClick={collapseControlSection} /> + )} -
+ ); }; diff --git a/frontend/src/components/Map/components/ControlSection/Layers.tsx b/frontend/src/components/Map/components/ControlSection/Layers.tsx new file mode 100644 index 000000000..33cfc7ef0 --- /dev/null +++ b/frontend/src/components/Map/components/ControlSection/Layers.tsx @@ -0,0 +1,45 @@ +import { GenericIconProps } from 'components/Icons/types'; +import React from 'react'; + +export const Layers: React.FC = ({ + color = 'currentColor', + opacity, + className, + size, +}) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/components/Map/components/DetailsMapDrawer/ArrowLeft.tsx b/frontend/src/components/Map/components/DetailsMapDrawer/ArrowLeft.tsx new file mode 100644 index 000000000..6e8f1360f --- /dev/null +++ b/frontend/src/components/Map/components/DetailsMapDrawer/ArrowLeft.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/DetailsMapDrawer/ArrowRight.tsx b/frontend/src/components/Map/components/DetailsMapDrawer/ArrowRight.tsx new file mode 100644 index 000000000..390038ed8 --- /dev/null +++ b/frontend/src/components/Map/components/DetailsMapDrawer/ArrowRight.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SvgComponent = (props: SVGProps) => ( + + + + +); + +export default SvgComponent; diff --git a/frontend/src/components/Map/components/DetailsMapDrawer/Siblings.tsx b/frontend/src/components/Map/components/DetailsMapDrawer/Siblings.tsx new file mode 100644 index 000000000..071bfb557 --- /dev/null +++ b/frontend/src/components/Map/components/DetailsMapDrawer/Siblings.tsx @@ -0,0 +1,65 @@ +import Link from 'components/Link'; +import { generateChildrenDetailsUrl } from 'components/pages/details/utils'; +import { TrekFamily } from 'modules/details/interface'; +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; +import { colorPalette } from 'stylesheet'; +import ArrowLeft from './ArrowLeft'; +import ArrowRight from './ArrowRight'; + +const Siblings = ({ trekFamily, trekId }: { trekFamily?: TrekFamily | null; trekId?: number }) => { + const intl = useIntl(); + + if (!trekFamily || !trekId || trekFamily.trekChildren.length < 2) { + return null; + } + + const index = trekFamily.trekChildren.findIndex(t => Number(t.id) === Number(trekId)); + const prev = index > 0 ? trekFamily.trekChildren[index - 1] : null; + const next = + index < trekFamily.trekChildren.length - 1 ? trekFamily.trekChildren[index + 1] : null; + + return ( + + + {prev && ( + + + {intl.formatMessage({ id: 'map.drawer.prev' })} + + )} + + + + {next && ( + + {intl.formatMessage({ id: 'map.drawer.next' })} + + + )} + + + ); +}; + +const Wrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; +const Prev = styled.div` + margin-right: 20px; +`; +const Next = styled.div` + margin-left: 20px; +`; +const Linkk = styled(Link)` + color: ${colorPalette.primary1} !important; + display: flex; + align-items: center; + + & > svg { + margin: 0 10px; + } +`; +export default Siblings; diff --git a/frontend/src/components/Map/components/DetailsMapDrawer/index.tsx b/frontend/src/components/Map/components/DetailsMapDrawer/index.tsx new file mode 100644 index 000000000..1f42c3223 --- /dev/null +++ b/frontend/src/components/Map/components/DetailsMapDrawer/index.tsx @@ -0,0 +1,78 @@ +import { TrekFamily } from 'modules/details/interface'; +import { useState } from 'react'; +import styled from 'styled-components'; +import { colorPalette } from 'stylesheet'; +import { AltimetricProfile } from '../AltimetricProfile'; +import Siblings from './Siblings'; + +const Wrapper = styled.div<{ open: boolean }>` + background: white; + position: absolute; + bottom: 0; + z-index: 1500; + + transform: translateY(${props => (props.open ? 0 : 'calc(100% - 65px)')}); + transition: transform 0.25s; + + width: 100vw; + border-top-left-radius: 15px; + border-top-right-radius: 15px; +`; + +const Puller = styled.div` + height: 65px; + display: flex; + flex-flow: column; + align-items: center; + justify-content: space-around; +`; + +const Content = styled.div` + height: 270px; +`; + +const Separator = styled.div` + border: 2px solid #d7d6d9; + width: 32px; + border-radius: 10px; + margin-top: 6px; +`; + +const Title = styled.div` + color: ${colorPalette.primary1}; + font-size: 16px; + font-weight: bold; + justify-self: stretch; + margin-bottom: 6px; + margin-top: 12px; +`; + +const DetailsMapDrawer: React.FC<{ + title: string; + trekGeoJSON?: string; + trekFamily?: TrekFamily | null; + trekId?: number; +}> = ({ title, trekGeoJSON, trekFamily, trekId }) => { + const [open, setOpen] = useState(false); + return ( + + { + if (e.isTrusted) { + setOpen(!open); + } + }} + > + + {title} + + + + {trekGeoJSON && } +
+
+
+ ); +}; + +export default DetailsMapDrawer; diff --git a/frontend/src/components/Map/components/HoverableMarker.tsx b/frontend/src/components/Map/components/HoverableMarker.tsx index 5ff4c8509..8eae7cb57 100644 --- a/frontend/src/components/Map/components/HoverableMarker.tsx +++ b/frontend/src/components/Map/components/HoverableMarker.tsx @@ -1,3 +1,4 @@ +import getActivityColor from 'components/pages/search/components/ResultCard/getActivityColor'; import { ListAndMapContext } from 'modules/map/ListAndMapContext'; import React, { ReactNode, useContext, useMemo } from 'react'; import { Marker } from 'react-leaflet'; @@ -14,12 +15,12 @@ interface BaseProps { interface TrekOrTouristicContentProps extends BaseProps { pictogramUri?: string; - type: 'TREK' | 'TOURISTIC_CONTENT' | 'OUTDOOR_SITE' | 'TOURISTIC_EVENT'; + type?: 'TREK' | 'TOURISTIC_CONTENT' | 'OUTDOOR_SITE' | 'TOURISTIC_EVENT' | null; } interface TrekChildProps extends BaseProps { rank: number; - type: 'TREK_CHILD'; + type?: 'TREK_CHILD'; } const isTrekChild = (trek: TrekOrTouristicContentProps | TrekChildProps): trek is TrekChildProps => @@ -28,6 +29,8 @@ const isTrekChild = (trek: TrekOrTouristicContentProps | TrekChildProps): trek i export const HoverableMarker = (props: TrekOrTouristicContentProps | TrekChildProps) => { const { hoveredCardId } = useContext(ListAndMapContext); const isCorrespondingCardHovered = props.id === hoveredCardId; + const color = getActivityColor(props.type); + return useMemo( () => ( {props.children} diff --git a/frontend/src/components/MobileFilterMenu/MobileFilterMenu.tsx b/frontend/src/components/MobileFilterMenu/MobileFilterMenu.tsx index c02c8d727..a758e294a 100644 --- a/frontend/src/components/MobileFilterMenu/MobileFilterMenu.tsx +++ b/frontend/src/components/MobileFilterMenu/MobileFilterMenu.tsx @@ -4,6 +4,9 @@ import React from 'react'; import Slide from 'react-burger-menu/lib/menus/slide'; import { Cross } from 'components/Icons/Cross'; +import getActivityColor from 'components/pages/search/components/ResultCard/getActivityColor'; +import { CATEGORY_ID, EVENT_ID, OUTDOOR_ID, PRACTICE_ID } from 'modules/filters/constant'; +import useCounter from 'components/pages/search/hooks/useCounter'; import { FilterCategory, FilterState } from '../../modules/filters/interface'; import { countFiltersSelected } from '../../modules/filters/utils'; @@ -17,6 +20,7 @@ interface Props { filtersList: FilterCategory[]; resetFilter: () => void; resultsNumber: number; + language: string; } export const MobileFilterMenu: React.FC = ({ @@ -26,7 +30,11 @@ export const MobileFilterMenu: React.FC = ({ resetFilter, resultsNumber, filtersList, + language, }) => { + const { treksCount, touristicContentsCount, outdoorSitesCount, touristicEventsCount } = + useCounter({ language }); + return ( /* * The library default behaviour is to have a fixed close icon which @@ -51,10 +59,15 @@ export const MobileFilterMenu: React.FC = ({
{filtersList.map(item => { - const numberSelected = countFiltersSelected(filtersState, item.filters, item.subFilters); + if (treksCount === 0 && item.id === PRACTICE_ID) return null; + if (touristicContentsCount === 0 && item.id === CATEGORY_ID) return null; + if (outdoorSitesCount === 0 && item.id === OUTDOOR_ID) return null; + if (touristicEventsCount === 0 && item.id === EVENT_ID) return null; + const numberSelected = countFiltersSelected(filtersState, item.filters, item.subFilters); return ( void; numberSelected: number; + color: string; } -export const MobileFilterMenuSection: React.FC = ({ title, onClick, numberSelected }) => { +export const MobileFilterMenuSection: React.FC = ({ + title, + onClick, + numberSelected, + color, +}) => { const classNameSectionName = `font-bold text-Mobile-C1 w-full ${ numberSelected > 0 ? 'text-primary1' : 'text-greyDarkColored' }`; @@ -18,12 +24,17 @@ export const MobileFilterMenuSection: React.FC = ({ title, onClick, numbe className="pt-4 pb-4 outline-none border-b border-solid border-greySoft flex items-center" > {numberSelected > 0 && ( -
+
{numberSelected}
)} -
{title}
- +
+ {title} +
+
); }; diff --git a/frontend/src/components/Popup/index.tsx b/frontend/src/components/Popup/index.tsx index 8b81ddbba..7dc3a4e89 100644 --- a/frontend/src/components/Popup/index.tsx +++ b/frontend/src/components/Popup/index.tsx @@ -1,15 +1,37 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { FormattedMessage } from 'react-intl'; +import { Cross } from 'components/Icons/Cross'; +import styled from 'styled-components'; + +interface PopupProps { + title?: string; + children: React.ReactNode; + onClose?: () => void; +} + +const PopupContent: React.FC = ({ children, onClose, title }) => { + useEffect(() => { + if (onClose === undefined) { + return; + } + const handleClose = (event: { key: string }): void => { + if (event.key === 'Escape') { + onClose(); + } + }; + global.addEventListener('keydown', handleClose); + return () => global.removeEventListener('keydown', handleClose); + }, [onClose]); -const Popup: React.FC = ({ children }) => { return ( -
-
+
+ ); }; +const Popup: React.FC = props => { + const [container] = useState(() => { + return document.createElement('div'); + }); + + useEffect(() => { + container.classList.add('popup-wrapper'); + document.body.appendChild(container); + return () => { + document.body.removeChild(container); + }; + }, []); + + return createPortal(, container); +}; + +export const Overlay = styled.div` + z-index: 901; +`; + +export const ClickOutside = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +`; + export default Popup; diff --git a/frontend/src/components/Report/Report.tsx b/frontend/src/components/Report/Report.tsx index 3f00178f5..a07555b8c 100644 --- a/frontend/src/components/Report/Report.tsx +++ b/frontend/src/components/Report/Report.tsx @@ -1,6 +1,7 @@ import { Button } from 'components/Button'; import InputRow from 'components/InputRow'; import TextareaRow from 'components/TextareaRow'; +import Popup from 'components/Popup'; import React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import Loader from 'react-loader'; @@ -24,123 +25,100 @@ const Report: React.FC = ({ trekId, startPoint, onRequestClose }) => { const intl = useIntl(); return ( -
-
-