diff --git a/package-lock.json b/package-lock.json index a28fbb5..87c0906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "i18next-resources-to-backend": "^1.2.1", "mongodb": "^6.8.0", "next": "^14.2.5", + "ol": "^10.0.0", "react": "18.3.1", "react-cookie": "^7.2.0", "react-dom": "18.3.1", @@ -1513,6 +1514,12 @@ "node": ">= 8" } }, + "node_modules/@petamoriken/float16": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", + "integrity": "sha512-/Ri4xDDpe12NT6Ex/DRgHzLlobiQXEW/hmG08w1wj/YU7hLemk97c+zHQFp0iZQ9r7YqgLEXZR2sls4HxBf9NA==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2930,6 +2937,40 @@ "dev": true, "license": "MIT" }, + "node_modules/color-parse": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.2.tgz", + "integrity": "sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + } + }, + "node_modules/color-parse/node_modules/color-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.0.tgz", + "integrity": "sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-rgba": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-3.0.0.tgz", + "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", + "license": "MIT", + "dependencies": { + "color-parse": "^2.0.0", + "color-space": "^2.0.0" + } + }, + "node_modules/color-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-space/-/color-space-2.0.1.tgz", + "integrity": "sha512-nKqUYlo0vZATVOFHY810BSYjmCARrG7e5R3UE3CQlyjJTvv5kSSmPG1kzm/oDyyqjehM+lW1RnEt9It9GNa5JA==", + "license": "MIT" + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -3433,6 +3474,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", + "license": "ISC" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4771,6 +4818,25 @@ "node": ">=6.9.0" } }, + "node_modules/geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "engines": { + "node": ">=10.19" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -5995,6 +6061,12 @@ "node": ">=0.10" } }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6687,6 +6759,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ol": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/ol/-/ol-10.0.0.tgz", + "integrity": "sha512-Gzfh61cQAxseCWL97VpGwbF91R2D69y3ABUewTl2H1Hjy6ipCtnoKshgO+n3WBrjsbsyS8QnkfmiJZNQGQNeOA==", + "license": "BSD-2-Clause", + "dependencies": { + "color-rgba": "^3.0.0", + "color-space": "^2.0.1", + "earcut": "^3.0.0", + "geotiff": "^2.0.7", + "pbf": "4.0.1", + "rbush": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6763,6 +6853,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6776,6 +6872,12 @@ "node": ">=6" } }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6905,6 +7007,18 @@ "node": ">= 14.16" } }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -7120,6 +7234,12 @@ "dev": true, "license": "MIT" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7164,6 +7284,33 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.0.tgz", + "integrity": "sha512-F5xw+166FYDZI6jEcz+sWEHL5/J+du3kQWkwqWrPKb6iVoLPZh+2KhTS4OoYqrw1v/RO1xQe6WsLwBvrUAlvXw==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -7372,6 +7519,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -9168,6 +9324,12 @@ "node": ">= 8" } }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -9486,6 +9648,12 @@ "node": ">=18" } }, + "node_modules/xml-utils": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.1.tgz", + "integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==", + "license": "CC0-1.0" + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -9522,6 +9690,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", + "license": "MIT AND BSD-3-Clause" } } } diff --git a/package.json b/package.json index b9821e3..e2da1d6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "i18next-resources-to-backend": "^1.2.1", "mongodb": "^6.8.0", "next": "^14.2.5", + "ol": "^10.0.0", "react": "18.3.1", "react-cookie": "^7.2.0", "react-dom": "18.3.1", diff --git a/src/api/map/index.ts b/src/api/map/index.ts new file mode 100644 index 0000000..77a288c --- /dev/null +++ b/src/api/map/index.ts @@ -0,0 +1,12 @@ +import type { MapCoordinates, MapResponse } from 'src/types/map' + + +export const getMapCoordinates = async (address: string): Promise => { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(address)}&format=json` + ) + const data: MapResponse[] = await response.json() + console.log('data', data) + + return [parseFloat(data[0].lon), parseFloat(data[0].lat)] +} diff --git a/src/app/[locale]/places/[placeId]/page.tsx b/src/app/[locale]/places/[placeId]/page.tsx index 3b07014..34a1423 100644 --- a/src/app/[locale]/places/[placeId]/page.tsx +++ b/src/app/[locale]/places/[placeId]/page.tsx @@ -4,7 +4,9 @@ import type { Metadata } from 'next' import styles from './place.module.css' +import { getMapCoordinates } from 'src/api/map' import { getPlace } from 'src/api/place' +import { GeoMap } from 'src/components/geo-map' import { SafeImage } from 'src/components/safe-image' import { getTranslationServer } from 'src/helpers/utils/getTranslationServer' import { isImageSecure } from 'src/helpers/utils/isImageSecure' @@ -31,6 +33,7 @@ export default async function PlacePage({ params }: PlacePageProps) { const { t } = await getTranslationServer({ locale: params.locale, namespace: 'places' }) const place = await getPlace(params.placeId) + const placeCoordinates = await getMapCoordinates(place.address) let placeMeta: PlaceMeta try { @@ -54,6 +57,10 @@ export default async function PlacePage({ params }: PlacePageProps) { +
+ +
+ {place.site && (

{t('form.website')}

diff --git a/src/components/geo-map/geo-map.tsx b/src/components/geo-map/geo-map.tsx new file mode 100644 index 0000000..9da8b3a --- /dev/null +++ b/src/components/geo-map/geo-map.tsx @@ -0,0 +1,63 @@ +'use client' + +import { Map, View } from 'ol' +import { Tile as TileLayer } from 'ol/layer' +import 'ol/ol.css' +import { fromLonLat } from 'ol/proj' +import { OSM } from 'ol/source' +import type { FC } from 'react' +import { useEffect, useRef } from 'react' + +import { MAP_ZOOM_LEVEL } from 'src/helpers/config/map' +import type { MapCoordinates } from 'src/types/map' + + +type GeoMapProps = { + address: string + coordinates: MapCoordinates +} + +export const GeoMap: FC = ({ coordinates }) => { + const mapContainerRef = useRef(null) + const mapRef = useRef(null) + + useEffect(() => { + if (coordinates !== null) { + const centerCoordinates = fromLonLat(coordinates) + + if (mapContainerRef.current && !mapRef.current) { + mapRef.current = new Map({ + layers: [new TileLayer({ source: new OSM() })], + view: new View({ + center: centerCoordinates, + zoom: MAP_ZOOM_LEVEL, + }), + target: mapContainerRef.current, + }) + } else if (mapRef.current) { + mapRef.current.getView().setCenter(centerCoordinates) + } + } + }, [coordinates]) + + useEffect(() => { + return () => { + if (mapRef.current) { + console.log('unmount') + mapRef.current.setTarget(undefined) + mapRef.current = null + } + } + }, []) + + if (coordinates === null) { + return null + } + + return ( +
+ ) +} diff --git a/src/components/geo-map/index.ts b/src/components/geo-map/index.ts new file mode 100644 index 0000000..ee60c2d --- /dev/null +++ b/src/components/geo-map/index.ts @@ -0,0 +1 @@ +export * from './geo-map' diff --git a/src/helpers/config/map.ts b/src/helpers/config/map.ts new file mode 100644 index 0000000..1a2deea --- /dev/null +++ b/src/helpers/config/map.ts @@ -0,0 +1 @@ +export const MAP_ZOOM_LEVEL = 19 diff --git a/src/types/map.d.ts b/src/types/map.d.ts new file mode 100644 index 0000000..5711fb6 --- /dev/null +++ b/src/types/map.d.ts @@ -0,0 +1,6 @@ +export type MapResponse = { + lat: string + lon: string +} + +export type MapCoordinates = [number, number]