diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..c5500558b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
+README.md
+.next
+.git
diff --git a/.gitignore b/.gitignore
index f1ccaeae2..0796d356d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,10 +2,12 @@
/coverage
/build
/dist
+/tsconfig.tsbuildinfo
.next
.DS_Store
.env
.idea
+next-env.d.ts
npm-debug.log*
yarn-debug.log*
yarn-error.log*
diff --git a/.gitpod.yml b/.gitpod.yml
new file mode 100644
index 000000000..a98171fc1
--- /dev/null
+++ b/.gitpod.yml
@@ -0,0 +1,3 @@
+tasks:
+ - init: yarn
+ command: yarn dev
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..3c032078a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+18
diff --git a/Dockerfile b/Dockerfile
index 7e8a042cf..3f3eee01e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,64 @@
-FROM mhart/alpine-node:10 as build-stage
+FROM node:18-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
+RUN \
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
+ elif [ -f package-lock.json ]; then npm ci; \
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+
+# Rebuild the source code only when needed
+FROM base AS builder
WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
COPY . .
-RUN yarn install --pure-lockfile
-RUN yarn build
-RUN yarn --production
-FROM mhart/alpine-node:base-10
+# Next.js collects completely anonymous telemetry data about general usage.
+# Learn more here: https://nextjs.org/telemetry
+# Uncomment the following line in case you want to disable telemetry during the build.
+# ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN NEXTJS_OUTPUT=standalone yarn build
+
+# Production image, copy all the files and run next
+FROM base AS runner
WORKDIR /app
-ENV NODE_ENV="production"
-COPY --from=build-stage /app .
+
+ENV NODE_ENV production
+# Uncomment the following line in case you want to disable telemetry during runtime.
+# ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+
+# Set the correct permission for prerender cache
+RUN mkdir .next
+RUN chown nextjs:nodejs .next
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
EXPOSE 3000
-CMD ["node", "./node_modules/.bin/next", "start"]
+ENV PORT 3000
+# set hostname to localhost
+ENV HOSTNAME "0.0.0.0"
+
+# server.js is created by next build from the standalone output
+# https://nextjs.org/docs/pages/api-reference/next-config-js/output
+CMD ["node", "server.js"]
diff --git a/README.md b/README.md
index f82c86aa4..f9ceb9296 100644
--- a/README.md
+++ b/README.md
@@ -20,10 +20,11 @@ You may [add issues](https://github.com/zbycz/osmapp/issues) here on github, or
## Features 🗺 📱 🖥
- **clickable map** – poi, cities, localities, ponds (more coming soon)
-- **info panel** – images from Wikipedia, Mapillary or Fody
+- **info panel** – images from Wikipedia, Mapillary or Fody, line numbers on public transport stops
- **editing** – for anonymous users inserts a note
- **search engine** – try for example "Tesco, London"
- **vector maps** – with the possibility of tilting to 3D (drag the compass, or two fingers drag)
+- **3D terrain** - tilt to 3D and then click terrain icon to show 3D terrain
- **tourist map** – from MapTiler: vector, including marked routes
- **layer switcher** – still basic, but you can add your own layers
- **mobile applications** – see [osmapp.org/install](https://osmapp.org/install)
diff --git a/next-env.d.ts b/next-env.d.ts
deleted file mode 100644
index 7b7aa2c77..000000000
--- a/next-env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
diff --git a/next.config.js b/next.config.js
index 876c85213..50b2131e4 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,12 +1,22 @@
/* eslint-disable */
const packageJson = require('./package.json');
-const withPWA = require('next-pwa');
+const withPWA = require('next-pwa')({
+ dest: 'public',
+});
-module.exports = withPWA({
- pwa: {
- dest: 'public',
- },
+const languages = {
+ de: 'Deutsch',
+ cs: 'Česky',
+ en: 'English',
+ es: 'Español',
+ fr: 'Français',
+ it: 'Italiano',
+ pl: 'Polski',
+ am: 'አማርኛ',
+};
+module.exports = withPWA({
+ output: process.env.NEXTJS_OUTPUT || undefined,
//TODO fails with current webpack config. Probably needs to get rid of sentry? (@sentry/nextjs was not cool)
// future: {
// webpack5: true,
@@ -15,25 +25,17 @@ module.exports = withPWA({
osmappVersion: packageJson.version.replace(/\.0$/, ''),
commitHash: (process.env.VERCEL_GIT_COMMIT_SHA || '').substr(0, 7),
commitMessage: process.env.VERCEL_GIT_COMMIT_MESSAGE || 'dev',
- languages: {
- de: 'Deutsch',
- cs: 'Česky',
- en: 'English',
- es: 'Español',
- fr: 'Français',
- it: 'Italiano',
- pl: 'Polski',
- am: 'አማርኛ',
- },
+ languages,
+ },
+ i18n: {
+ // we let next only handle URL, but chosen locale is in getServerIntl()
+ locales: ['default', ...Object.keys(languages)],
+ defaultLocale: 'default',
+ localeDetection: false,
},
webpack: (config, { dev, isServer }) => {
- // Fixes npm packages that depend on `fs` module
- config.node = {
- fs: 'empty',
- };
-
if (!dev) {
- config.devtool = 'source-maps';
+ config.devtool = 'source-map';
for (const plugin of config.optimization.minimizer) {
if (plugin.constructor.name === 'TerserPlugin') {
plugin.options.sourceMap = true;
diff --git a/package.json b/package.json
index 7103c0650..884b8af24 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,12 @@
{
"name": "osmapp",
"version": "1.3.0",
+ "engines": {
+ "node": "^18"
+ },
"scripts": {
"dev": "next",
+ "test": "jest",
"lint": "eslint . --report-unused-disable-directives",
"lintfix": "prettier . --write && eslint ./src ./pages --report-unused-disable-directives --fix",
"prettify": "prettier . --write",
@@ -28,9 +32,9 @@
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "4.0.0-alpha.58",
+ "@openstreetmap/id-tagging-schema": "^6.1.0",
"@sentry/browser": "^6.5.1",
"@sentry/node": "^6.5.1",
- "@types/maplibre-gl": "^1.13.1",
"accept-language-parser": "^1.5.0",
"autosuggest-highlight": "^3.1.1",
"isomorphic-unfetch": "^3.1.0",
@@ -39,20 +43,21 @@
"js-cookie": "^2.2.1",
"jss": "^10.6.0",
"lodash": "^4.17.21",
- "maplibre-gl": "^1.14.0",
- "next": "^10.2.3",
+ "maplibre-gl": "^3.3.1",
+ "next": "^13.4.3",
"next-cookies": "^2.0.3",
"next-pwa": "^5.2.21",
"osm-auth": "^1.1.1",
- "react": "^17.0.2",
+ "react": "^18.2.0",
"react-custom-scrollbars": "^4.2.1",
- "react-dom": "^17.0.2",
+ "react-dom": "^18.2.0",
"react-jss": "^10.6.0",
"simple-opening-hours": "^0.1.1",
"styled-components": "^5.3.0",
"styled-jsx": "^3.4.4"
},
"devDependencies": {
+ "@types/autosuggest-highlight": "^3.2.0",
"@types/jest": "^26.0.23",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"babel-eslint": "^10.1.0",
@@ -67,6 +72,6 @@
"husky": "^4",
"lint-staged": "^11.0.0",
"prettier": "^2.3.0",
- "typescript": "^4.3.2"
+ "typescript": "^5.0.4"
}
}
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 27b0c3788..1c471ccfd 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -1,5 +1,11 @@
import React from 'react';
-import Document, { Head, Html, Main, NextScript } from 'next/document';
+import Document, {
+ DocumentInitialProps,
+ Head,
+ Html,
+ Main,
+ NextScript,
+} from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import { ServerStyleSheet } from 'styled-components';
import { getServerIntl } from '../src/services/intlServer';
@@ -8,7 +14,7 @@ import { Favicons } from '../src/helpers/Favicons';
export default class MyDocument extends Document {
render() {
- const { serverIntl } = this.props as any;
+ const { serverIntl, asPath } = this.props as any;
return (
@@ -23,6 +29,16 @@ export default class MyDocument extends Document {
+ {/* we dont need this to change after SSR */}
+ {Object.keys(serverIntl.languages).map((lang) => (
+
+ ))}
+
{/* */}
@@ -83,5 +99,6 @@ MyDocument.getInitialProps = async (ctx) => {
sheets2.getStyleElement(),
],
serverIntl,
- };
+ asPath: ctx.asPath,
+ } as DocumentInitialProps;
};
diff --git a/pages/sitemap.txt.tsx b/pages/sitemap.txt.tsx
new file mode 100644
index 000000000..3d9db7cf4
--- /dev/null
+++ b/pages/sitemap.txt.tsx
@@ -0,0 +1,19 @@
+import { fetchText } from '../src/services/fetch';
+
+const Sitemap = () => null;
+
+export const getServerSideProps = async ({ res }) => {
+ const content = await fetchText(
+ 'https://zbycz.github.io/osm-static/sitemap.txt',
+ );
+
+ res.setHeader('Content-Type', 'text/plain');
+ res.write(content);
+ res.end();
+
+ return {
+ props: {},
+ };
+};
+
+export default Sitemap;
diff --git a/src/assets/WikipediaIcon.tsx b/src/assets/WikipediaIcon.tsx
new file mode 100644
index 000000000..686a67c3c
--- /dev/null
+++ b/src/assets/WikipediaIcon.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+// source https://icon-sets.iconify.design/tabler/brand-wikipedia/
+// license MIT
+export const WikipediaIcon = (props) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+);
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 88fbc6784..22a26e17e 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -3,7 +3,9 @@ import Cookies from 'js-cookie';
import nextCookies from 'next-cookies';
import Router, { useRouter } from 'next/router';
-import FeaturePanel from '../FeaturePanel/FeaturePanel';
+import { Snackbar } from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import { FeaturePanel } from '../FeaturePanel/FeaturePanel';
import Map from '../Map/Map';
import SearchBox from '../SearchBox/SearchBox';
import { MapStateProvider, useMapStateContext } from '../utils/MapStateContext';
@@ -16,6 +18,7 @@ import { FeaturePreview } from '../FeaturePreview/FeaturePreview';
import { TitleAndMetaTags } from '../../helpers/TitleAndMetaTags';
import { InstallDialog } from '../HomepagePanel/InstallDialog';
import { setIntlForSSR } from '../../services/intl';
+import { EditDialogProvider } from '../FeaturePanel/helpers/EditDialogContext';
const usePersistMapView = () => {
const { view } = useMapStateContext();
@@ -66,6 +69,14 @@ const IndexWithProviders = () => {
usePersistMapView();
useUpdateViewFromHash();
+ // temporary Alert until the issue is fixed
+ const [brokenShown, setBrokenShown] = React.useState(true);
+ const onBrokenClose = (event?: React.SyntheticEvent, reason?: string) => {
+ if (reason !== 'clickaway') {
+ setBrokenShown(false);
+ }
+ };
+
// TODO add correct error boundaries
return (
<>
@@ -77,6 +88,21 @@ const IndexWithProviders = () => {
{preview && }
+
+ {!featureShown && !preview && (
+
+
+ Some clickable POIs are broken on Maptiler –{' '}
+
+ issue here
+
+ .
+
+
+ )}
>
);
};
@@ -87,7 +113,9 @@ const App = ({ featureFromRouter, initialMapView, hpCookie }) => {
-
+
+
+
diff --git a/src/components/FeaturePanel/Coordinates.tsx b/src/components/FeaturePanel/Coordinates.tsx
index fc83f606f..577547dce 100644
--- a/src/components/FeaturePanel/Coordinates.tsx
+++ b/src/components/FeaturePanel/Coordinates.tsx
@@ -59,14 +59,14 @@ const LinkItem = ({ href, label }) => (
// Our map uses 512 tiles, so our zoom is "one less"
// https://wiki.openstreetmap.org/wiki/Zoom_levels#Mapbox_GL
-const MAPBOXGL_ZOOM_DIFFERENCE = 1;
+const MAPLIBREGL_ZOOM_DIFFERENCE = 1;
const useGetItems = ([lon, lat]: PositionBoth) => {
const { feature } = useFeatureContext();
const { view } = useMapStateContext();
const [ourZoom] = view;
- const zoom = parseFloat(ourZoom) + MAPBOXGL_ZOOM_DIFFERENCE;
+ const zoom = parseFloat(ourZoom) + MAPLIBREGL_ZOOM_DIFFERENCE;
const zoomInt = Math.round(zoom);
const osmQuery = feature?.osmMeta?.id
? `${feature.osmMeta.type}/${feature.osmMeta.id}`
@@ -106,7 +106,7 @@ export const Coords = ({ coords }: Props) => {
const osmappLink = getFullOsmappLink(feature);
return (
-
+
{positionToDeg(coords)}
-
+
);
};
diff --git a/src/components/FeaturePanel/EditButton.tsx b/src/components/FeaturePanel/EditButton.tsx
index 8ab624e1a..f41aaf5df 100644
--- a/src/components/FeaturePanel/EditButton.tsx
+++ b/src/components/FeaturePanel/EditButton.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import { Box } from '@material-ui/core';
import { t } from '../../services/intl';
import { useOsmAuthContext } from '../utils/OsmAuthContext';
+import { useEditDialogContext } from './helpers/EditDialogContext';
const getLabel = (loggedIn, isAddPlace, isUndelete) => {
if (isAddPlace) return t('featurepanel.add_place_button');
@@ -13,8 +14,9 @@ const getLabel = (loggedIn, isAddPlace, isUndelete) => {
return t('featurepanel.note_button');
};
-export const EditButton = ({ isAddPlace, isUndelete, setDialogOpenedWith }) => {
+export const EditButton = ({ isAddPlace, isUndelete }) => {
const { loggedIn } = useOsmAuthContext();
+ const { open } = useEditDialogContext();
return (
@@ -25,7 +27,7 @@ export const EditButton = ({ isAddPlace, isUndelete, setDialogOpenedWith }) => {
}
variant="outlined"
color="primary"
- onClick={() => setDialogOpenedWith(true)}
+ onClick={open}
>
{getLabel(loggedIn, isAddPlace, isUndelete)}
diff --git a/src/components/FeaturePanel/EditDialog/EditDialog.tsx b/src/components/FeaturePanel/EditDialog/EditDialog.tsx
index ea79044a0..3d3f13d77 100644
--- a/src/components/FeaturePanel/EditDialog/EditDialog.tsx
+++ b/src/components/FeaturePanel/EditDialog/EditDialog.tsx
@@ -31,6 +31,7 @@ import Maki from '../../utils/Maki';
import { FeatureTypeSelect } from './FeatureTypeSelect';
import { getLabel } from '../../../helpers/featureLabel';
import { useUserThemeContext } from '../../../helpers/theme';
+import { useEditDialogContext } from '../helpers/EditDialogContext';
const useIsFullScreen = () => {
const theme = useTheme();
@@ -62,9 +63,6 @@ const StyledDialog = styled(Dialog)`
interface Props {
feature: Feature;
- open: boolean;
- handleClose: () => void;
- focusTag: boolean | string;
isAddPlace: boolean;
isUndelete: boolean;
}
@@ -119,17 +117,12 @@ const saveDialog = ({
});
};
-export const EditDialog = ({
- feature,
- open,
- handleClose,
- focusTag,
- isAddPlace,
- isUndelete,
-}: Props) => {
+export const EditDialog = ({ feature, isAddPlace, isUndelete }: Props) => {
const { currentTheme } = useUserThemeContext();
const router = useRouter();
const { loggedIn, handleLogout } = useOsmAuthContext();
+ const { opened, close, focusTag } = useEditDialogContext();
+
const fullScreen = useIsFullScreen();
const [typeTag, setTypeTag] = useState('');
const [tags, setTag] = useTagsState(feature.tags); // TODO all these should go into `values`, consider Formik
@@ -141,7 +134,7 @@ export const EditDialog = ({
const [successInfo, setSuccessInfo] = useState(false);
const onClose = () => {
- handleClose();
+ close();
if (successInfo.redirect) {
router.replace(successInfo.redirect); // only useRouter reloads the panel client-side
}
@@ -168,7 +161,7 @@ export const EditDialog = ({
return (
diff --git a/src/components/FeaturePanel/FeatureHeading.tsx b/src/components/FeaturePanel/FeatureHeading.tsx
index bf857a31a..5e816b128 100644
--- a/src/components/FeaturePanel/FeatureHeading.tsx
+++ b/src/components/FeaturePanel/FeatureHeading.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import { EditIconButton } from './helpers/EditIconButton';
+import { useEditDialogContext } from './helpers/EditDialogContext';
const Wrapper = styled.div`
font-size: 36px;
@@ -15,9 +16,13 @@ const Wrapper = styled.div`
}
`;
-export const FeatureHeading = ({ title, onEdit, deleted, editEnabled }) => (
-
- {editEnabled && onEdit('name')} />}
- {title}
-
-);
+export const FeatureHeading = ({ title, deleted, editEnabled }) => {
+ const { openWithTag } = useEditDialogContext();
+
+ return (
+
+ {editEnabled && openWithTag('name')} />}
+ {title}
+
+ );
+};
diff --git a/src/components/FeaturePanel/FeaturePanel.tsx b/src/components/FeaturePanel/FeaturePanel.tsx
index ce046c213..b3f956fcc 100644
--- a/src/components/FeaturePanel/FeaturePanel.tsx
+++ b/src/components/FeaturePanel/FeaturePanel.tsx
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { FeatureHeading } from './FeatureHeading';
import Coordinates from './Coordinates';
import { useToggleState } from '../helpers';
-import { TagsTable } from './TagsTable';
import { getFullOsmappLink, getUrlOsmId } from '../../services/helpers';
import { EditDialog } from './EditDialog/EditDialog';
import {
@@ -18,37 +17,24 @@ import { ObjectsAround } from './ObjectsAround';
import { OsmError } from './OsmError';
import { Members } from './Members';
import { EditButton } from './EditButton';
-import { FeaturedTags } from './FeaturedTags';
import { FeatureOpenPlaceGuideLink } from './FeatureOpenPlaceGuideLink';
import { getLabel } from '../../helpers/featureLabel';
import { ImageSection } from './ImageSection/ImageSection';
+import { PublicTransport } from './PublicTransport/PublicTransport';
+import { Properties } from './Properties/Properties';
-const featuredKeys = [
- 'website',
- 'contact:website',
- 'phone',
- 'contact:phone',
- 'contact:mobile',
- 'opening_hours',
- 'description',
-];
-
-const FeaturePanel = () => {
+export const FeaturePanel = () => {
const { feature } = useFeatureContext();
const [advanced, setAdvanced] = useState(false);
const [showAround, toggleShowAround] = useToggleState(false);
- const [dialogOpenedWith, setDialogOpenedWith] =
- useState(false);
+ const [showTags, toggleShowTags] = useToggleState(false);
- const { point, tags, osmMeta, skeleton, error } = feature;
- const deleted = error === 'deleted';
- const editEnabled = !skeleton && (!error || deleted);
+ const { point, tags, osmMeta, skeleton, deleted } = feature;
+ const editEnabled = !skeleton;
+ const showTagsTable = deleted || showTags || (!skeleton && !feature.schema);
const osmappLink = getFullOsmappLink(feature);
- const featuredTags = featuredKeys
- .map((k) => [k, tags[k]])
- .filter(([, v]) => v);
const label = getLabel(feature);
return (
@@ -60,65 +46,60 @@ const FeaturePanel = () => {
deleted={deleted}
title={label}
editEnabled={editEnabled && !point}
- onEdit={setDialogOpenedWith}
/>
-
+ {!skeleton && (
+ <>
+
-
+
-
+
-
+ {advanced && }
- {advanced && }
+
- {editEnabled && (
- <>
-
+ {editEnabled && (
+ <>
+
- setDialogOpenedWith(false)}
- feature={feature}
- isAddPlace={point}
- isUndelete={deleted}
- focusTag={dialogOpenedWith}
- key={
- getUrlOsmId(osmMeta) + (deleted && 'del') // we need to refresh inner state
- }
- />
+
+ >
+ )}
+
+ {point && }
>
)}
- {point && }
-
{osmappLink}
+ {' '}
-
{!point && showAround && }
@@ -136,5 +116,3 @@ const FeaturePanel = () => {
);
};
-
-export default FeaturePanel;
diff --git a/src/components/FeaturePanel/FeaturedTag.tsx b/src/components/FeaturePanel/FeaturedTag.tsx
index f68d327ee..34019fa87 100644
--- a/src/components/FeaturePanel/FeaturedTag.tsx
+++ b/src/components/FeaturePanel/FeaturedTag.tsx
@@ -5,6 +5,9 @@ import WebsiteRenderer from './renderers/WebsiteRenderer';
import OpeningHoursRenderer from './renderers/OpeningHoursRenderer';
import PhoneRenderer from './renderers/PhoneRenderer';
import { EditIconButton } from './helpers/EditIconButton';
+import { FoodHygieneRatingSchemeRenderer } from './renderers/FoodHygieneRatingScheme';
+import { WikipediaRenderer } from './renderers/WikipediaRenderer';
+import { WikidataRenderer } from './renderers/WikidataRenderer';
const Wrapper = styled.div`
position: relative;
@@ -26,16 +29,26 @@ const Value = styled.div`
margin: 0 10px -6px 2px;
opacity: 0.4;
}
+
+ :last-child {
+ min-width: 0;
+ overflow: hidden;
+ }
`;
const DefaultRenderer = ({ v }) => v;
-const renderers = {
+const renderers: {
+ [key: string]: React.FC<{ k: string; v: string }>;
+} = {
website: WebsiteRenderer,
'contact:website': WebsiteRenderer,
phone: PhoneRenderer,
'contact:phone': PhoneRenderer,
'contact:mobile': PhoneRenderer,
opening_hours: OpeningHoursRenderer,
+ 'fhrs:id': FoodHygieneRatingSchemeRenderer,
+ wikipedia: WikipediaRenderer,
+ wikidata: WikidataRenderer,
};
export const FeaturedTag = ({ k, v, onEdit }) => {
@@ -46,7 +59,7 @@ export const FeaturedTag = ({ k, v, onEdit }) => {
onEdit(k)} />
-
+
);
diff --git a/src/components/FeaturePanel/FeaturedTags.tsx b/src/components/FeaturePanel/FeaturedTags.tsx
index d9ff4a818..8eb49cc05 100644
--- a/src/components/FeaturePanel/FeaturedTags.tsx
+++ b/src/components/FeaturePanel/FeaturedTags.tsx
@@ -1,26 +1,23 @@
-import Typography from '@material-ui/core/Typography';
import React from 'react';
import styled from 'styled-components';
-import { t } from '../../services/intl';
import { FeaturedTag } from './FeaturedTag';
+import { useEditDialogContext } from './helpers/EditDialogContext';
const Spacer = styled.div`
padding-bottom: 10px;
`;
-export const FeaturedTags = ({ featuredTags, setDialogOpenedWith }) => {
+export const FeaturedTags = ({ featuredTags }) => {
+ const { openWithTag } = useEditDialogContext();
+
if (!featuredTags.length) return null;
return (
<>
{featuredTags.map(([k, v]) => (
-
+
))}
-
-
- {t('featurepanel.other_info_heading')}
-
>
);
};
diff --git a/src/components/FeaturePanel/ImageSection/ImageSection.tsx b/src/components/FeaturePanel/ImageSection/ImageSection.tsx
index b38045b91..9654dc74d 100644
--- a/src/components/FeaturePanel/ImageSection/ImageSection.tsx
+++ b/src/components/FeaturePanel/ImageSection/ImageSection.tsx
@@ -6,11 +6,9 @@ import React from 'react';
import styled from 'styled-components';
import { useFeatureContext } from '../../utils/FeatureContext';
import { FeatureImage } from './FeatureImage';
-import Maki from '../../utils/Maki';
-import { hasName } from '../../../helpers/featureLabel';
import { t } from '../../../services/intl';
import { SHOW_PROTOTYPE_UI } from '../../../config';
-import { Feature } from '../../../services/types';
+import { PoiDescription } from './PoiDescription';
const StyledIconButton = styled(IconButton)`
svg {
@@ -20,40 +18,13 @@ const StyledIconButton = styled(IconButton)`
}
`;
-const PoiType = styled.div`
- color: #fff;
- margin: 0 auto 0 15px;
- font-size: 13px;
- position: relative;
- width: 100%;
- svg {
- vertical-align: bottom;
- }
- span {
- position: absolute;
- left: 20px;
- }
-`;
-
-const getSubclass = ({ layer, osmMeta, properties }: Feature) =>
- properties.subclass?.replace(/_/g, ' ') ||
- (layer && layer.id) || // layer.id specified only when maplibre-gl skeleton displayed
- osmMeta.type;
-
export const ImageSection = () => {
const { feature } = useFeatureContext();
const { properties } = feature;
- const poiType = hasName(feature)
- ? getSubclass(feature)
- : t('featurepanel.no_name');
-
return (
-
-
- {poiType}
-
+
{SHOW_PROTOTYPE_UI && (
<>
diff --git a/src/components/FeaturePanel/ImageSection/PoiDescription.tsx b/src/components/FeaturePanel/ImageSection/PoiDescription.tsx
new file mode 100644
index 000000000..2feeab9b3
--- /dev/null
+++ b/src/components/FeaturePanel/ImageSection/PoiDescription.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import styled from 'styled-components';
+import { hasName } from '../../../helpers/featureLabel';
+import { useFeatureContext } from '../../utils/FeatureContext';
+import { t } from '../../../services/intl';
+import Maki from '../../utils/Maki';
+import { Feature } from '../../../services/types';
+
+const PoiType = styled.div`
+ color: #fff;
+ margin: 0 auto 0 15px;
+ font-size: 13px;
+ position: relative;
+ width: 100%;
+
+ svg {
+ vertical-align: bottom;
+ }
+
+ span {
+ position: absolute;
+ left: 20px;
+ }
+`;
+
+const getSubclass = ({ layer, osmMeta, properties, schema }: Feature) =>
+ schema?.label ||
+ properties.subclass?.replace(/_/g, ' ') ||
+ (layer && layer.id) || // layer.id specified only when maplibre-gl skeleton displayed
+ osmMeta.type;
+
+export const PoiDescription = () => {
+ const { feature } = useFeatureContext();
+ const { properties } = feature;
+
+ const poiType = hasName(feature)
+ ? getSubclass(feature)
+ : t('featurepanel.no_name');
+
+ return (
+
+
+ {poiType}
+
+ );
+};
diff --git a/src/components/FeaturePanel/OsmError.tsx b/src/components/FeaturePanel/OsmError.tsx
index 85a63d1be..794c03e4f 100644
--- a/src/components/FeaturePanel/OsmError.tsx
+++ b/src/components/FeaturePanel/OsmError.tsx
@@ -9,7 +9,7 @@ export const OsmError = () => {
const { feature } = useFeatureContext();
const code = feature.error;
- if (code === 'deleted') {
+ if (feature.deleted) {
return (
@@ -52,5 +52,13 @@ export const OsmError = () => {
);
}
+ if (Object.keys(feature.tags).length === 0 && !feature.point) {
+ return (
+
+ {t('featurepanel.info_no_tags')}
+
+ );
+ }
+
return null;
};
diff --git a/src/components/FeaturePanel/Properties/IdSchemeFields.tsx b/src/components/FeaturePanel/Properties/IdSchemeFields.tsx
new file mode 100644
index 000000000..d03ca6a0d
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/IdSchemeFields.tsx
@@ -0,0 +1,173 @@
+import React, { ReactNode } from 'react';
+import styled from 'styled-components';
+import { Field } from '../../../services/tagging/types/Fields';
+import { useToggleState } from '../../helpers';
+import { buildAddress } from '../../../services/helpers';
+import { Feature } from '../../../services/types';
+import { t } from '../../../services/intl';
+import { TagsTableInner } from './TagsTableInner';
+import { EditIconButton } from '../helpers/EditIconButton';
+import { useEditDialogContext } from '../helpers/EditDialogContext';
+import { renderValue } from './renderValue';
+import { Table } from './Table';
+import { ShowMoreButton } from './helpers';
+import { useFeatureContext } from '../../utils/FeatureContext';
+import { Subheading } from '../helpers/Subheading';
+import { UiField } from '../../../services/tagging/types/Presets';
+
+const Spacer = styled.div`
+ width: 100%;
+ height: 50px;
+`;
+
+const render = (uiField: UiField, feature: Feature): string | ReactNode => {
+ const { field, key: k, value: v, tagsForField, fieldTranslation } = uiField;
+
+ if (field.type === 'address') {
+ return buildAddress(feature.tags, feature.center);
+ }
+
+ if (field.fieldKey === 'wikidata') {
+ return renderValue('wikidata', feature.tags.wikidata);
+ }
+
+ if (fieldTranslation?.types && fieldTranslation?.options) {
+ return tagsForField.map(({ key, value: value2 }) => (
+
+ {fieldTranslation.types[key]}:{' '}
+ {renderValue(key, fieldTranslation.options[value2]?.title)}
+
+ ));
+ }
+
+ if (field?.type === 'manyCombo') {
+ return tagsForField.map(({ key, value: value2 }) => (
+
+ {fieldTranslation.options[key]}:{' '}
+ {renderValue(key, fieldTranslation.options[value2]?.title ?? value2)}
+
+ ));
+ }
+
+ if (tagsForField?.length >= 2) {
+ return (
+ <>
+ {tagsForField.map(({ key, value: value2 }) => (
+ {renderValue(key, value2)}
+ ))}
+ >
+ );
+ }
+
+ if (!k) {
+ return renderValue(tagsForField[0].key, tagsForField[0].value);
+ }
+
+ return renderValue(k, v);
+};
+
+// TODO some fields eg. oneway/bicycle doesnt have units in brackets
+const unitRegExp = / \((.+)\)$/i;
+const removeUnits = (label) => label.replace(unitRegExp, '');
+const addUnits = (label, value: string | ReactNode) => {
+ if (typeof value !== 'string') return value;
+ const unit = label.match(unitRegExp);
+ return `${value}${unit ? ` (${unit[1]})` : ''}`;
+};
+
+const getTooltip = (field: Field, key: string) =>
+ `field: ${field.fieldKey}${key === field.fieldKey ? '' : `, key: ${key}`} (${
+ field.type
+ })`;
+
+const UiFields = ({ fields }: { fields: UiField[] }) => {
+ const { openWithTag } = useEditDialogContext();
+ const { feature } = useFeatureContext();
+
+ return (
+ <>
+ {fields.map((uiField) => {
+ const { key, label, field, tagsForField } = uiField;
+ return (
+
+ {removeUnits(label)} |
+
+ openWithTag(tagsForField?.[0]?.key ?? key)}
+ />
+ {addUnits(label, render(uiField, feature))}
+ |
+
+ );
+ })}
+ >
+ );
+};
+
+const OtherTagsSection = () => {
+ const [otherTagsShown, toggleOtherTagsShown] = useToggleState(false);
+ const { feature } = useFeatureContext();
+ const { schema } = feature;
+
+ return (
+ <>
+
+
+
+ |
+
+ {otherTagsShown && (
+ <>
+
+ ({ ...acc, [key]: feature.tags[key] }),
+ {},
+ )}
+ center={feature.center}
+ />
+ >
+ )}
+ >
+ );
+};
+
+export const IdSchemeFields = () => {
+ const { feature } = useFeatureContext();
+ const { schema } = feature;
+ const { keysTodo, featuredTags, matchedFields, tagsWithFields } = schema;
+
+ // TODO add link to osm key reference as Tooltip https://wiki.openstreetmap.org/w/api.php?action=wbgetentities&format=json&languagefallback=1&languages=en%7Ccs%7Cen-us%7Csk&origin=*&sites=wiki&titles=Locale%3Acs%7CLocale%3Aen-us%7CLocale%3Ask%7CKey%3Astart%20date%7CTag%3Astart%20date%3D1752
+ // TODO preset translations https://github.com/zbycz/osmapp/issues/190
+
+ const numberOfItems =
+ featuredTags.length +
+ matchedFields.length +
+ tagsWithFields.length +
+ keysTodo.length;
+
+ if (!numberOfItems) {
+ return ;
+ }
+
+ const showDetailsHeading = !!(featuredTags.length && matchedFields.length);
+ const showOtherTagsSection = !!(tagsWithFields.length || keysTodo.length);
+
+ return (
+ <>
+ {showDetailsHeading && (
+ {t('featurepanel.details_heading')}
+ )}
+
+
+
+
+ {showOtherTagsSection && }
+
+
+ >
+ );
+};
diff --git a/src/components/FeaturePanel/Properties/Properties.tsx b/src/components/FeaturePanel/Properties/Properties.tsx
new file mode 100644
index 000000000..a4237c110
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/Properties.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { FeaturedTags } from '../FeaturedTags';
+import { IdSchemeFields } from './IdSchemeFields';
+import { t } from '../../../services/intl';
+import { TagsTableInner } from './TagsTableInner';
+import { useFeatureContext } from '../../utils/FeatureContext';
+import { Subheading } from '../helpers/Subheading';
+import { Wrapper } from './Wrapper';
+import { Table } from './Table';
+
+export const Properties = ({ showTags }) => {
+ const { feature } = useFeatureContext();
+
+ return (
+ <>
+ {!showTags && (
+ <>
+
+
+ >
+ )}
+ {showTags && (
+ <>
+ {!!Object.keys(feature.tags).length && (
+ {t('featurepanel.all_tags_heading')}
+ )}
+
+
+
+ >
+ )}
+ >
+ );
+};
diff --git a/src/components/FeaturePanel/Properties/Table.tsx b/src/components/FeaturePanel/Properties/Table.tsx
new file mode 100644
index 000000000..b680c5a93
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/Table.tsx
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+
+export const Table = styled.table`
+ font-size: 1rem;
+ width: 100%;
+
+ th,
+ td {
+ padding: 0.1em;
+ overflow: hidden;
+ vertical-align: baseline;
+
+ &:hover .show-on-hover {
+ display: block !important;
+ }
+ }
+
+ th {
+ width: 140px;
+ max-width: 140px;
+ color: ${({ theme }) => theme.palette.text.secondary};
+ text-align: left;
+ font-weight: normal;
+ vertical-align: baseline;
+ padding-left: 0;
+ }
+
+ table {
+ padding-left: 1em;
+ padding-bottom: 1em;
+ }
+`;
diff --git a/src/components/FeaturePanel/Properties/TagsTableInner.tsx b/src/components/FeaturePanel/Properties/TagsTableInner.tsx
new file mode 100644
index 000000000..ac6b28d75
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/TagsTableInner.tsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import truncate from 'lodash/truncate';
+
+import { useToggleState } from '../../helpers';
+import { EditIconButton } from '../helpers/EditIconButton';
+import { buildAddress } from '../../../services/helpers';
+import { ToggleButton } from '../helpers/ToggleButton';
+import { renderValue } from './renderValue';
+import { useEditDialogContext } from '../helpers/EditDialogContext';
+
+const isAddr = (k) => k.match(/^addr:|uir_adr|:addr/);
+const isName = (k) => k.match(/^name(:|$)/);
+const isShortName = (k) => k.match(/^short_name(:|$)/);
+const isAltName = (k) => k.match(/^alt_name(:|$)/);
+const isOfficialName = (k) => k.match(/^official_name(:|$)/);
+const isOldName = (k) => k.match(/^old_name(:|$)/);
+const isBuilding = (k) =>
+ k.match(/building|roof|^min_level|^max_level|height$/);
+const isNetwork = (k) => k.match(/network/);
+const isBrand = (k) => k.match(/^brand/);
+const isOperator = (k) => k.match(/^operator/);
+const isPayment = (k) => k.match(/^payment/);
+
+const TagsGroup = ({ tags, label, value, hideArrow = false, onEdit }) => {
+ const [isShown, toggle] = useToggleState(false);
+
+ if (!tags.length) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {label} |
+
+ onEdit(tags[0][0])} />
+ {value || tags[0]?.[1]}
+ {!hideArrow && }
+ |
+
+ {isShown && (
+
+
+
+
+ {tags.map(([k, v]) => (
+
+ {k} |
+ {renderValue(k, v)} |
+
+ ))}
+
+
+ |
+
+ )}
+ >
+ );
+};
+
+// TODO make it dynamic - count how many "first parts before :" are there, and group all >= 2
+
+export const TagsTableInner = ({ tags, center, except = [] }) => {
+ const { openWithTag: onEdit } = useEditDialogContext();
+
+ const tagsEntries = Object.entries(tags).filter(([k]) => !except.includes(k));
+
+ const addrs = tagsEntries.filter(([k]) => isAddr(k));
+ const names = tagsEntries.filter(([k]) => isName(k));
+ const shortNames = tagsEntries.filter(([k]) => isShortName(k));
+ const altNames = tagsEntries.filter(([k]) => isAltName(k));
+ const officialNames = tagsEntries.filter(([k]) => isOfficialName(k));
+ const oldNames = tagsEntries.filter(([k]) => isOldName(k));
+ const buildings = tagsEntries.filter(([k]) => isBuilding(k));
+ const networks = tagsEntries.filter(([k]) => isNetwork(k));
+ const brands = tagsEntries.filter(([k]) => isBrand(k));
+ const operator = tagsEntries.filter(([k]) => isOperator(k));
+ const payments = tagsEntries.filter(([k]) => isPayment(k));
+ const rest = tagsEntries.filter(
+ ([k]) =>
+ !isName(k) &&
+ !isShortName(k) &&
+ !isAltName(k) &&
+ !isOfficialName(k) &&
+ !isOldName(k) &&
+ !isAddr(k) &&
+ !isBuilding(k) &&
+ !isNetwork(k) &&
+ !isOperator(k) &&
+ !isPayment(k) &&
+ !isBrand(k),
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ {rest.map(([k, v]) => (
+
+ {k} |
+
+ onEdit(k)} />
+ {renderValue(k, v)}
+ |
+
+ ))}
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/FeaturePanel/Properties/Wrapper.tsx b/src/components/FeaturePanel/Properties/Wrapper.tsx
new file mode 100644
index 000000000..b35e02735
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/Wrapper.tsx
@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+ position: relative;
+ margin-bottom: 2em;
+`;
diff --git a/src/components/FeaturePanel/helpers/getUrlForTag.tsx b/src/components/FeaturePanel/Properties/getUrlForTag.tsx
similarity index 86%
rename from src/components/FeaturePanel/helpers/getUrlForTag.tsx
rename to src/components/FeaturePanel/Properties/getUrlForTag.tsx
index 90be731f7..4be68437c 100644
--- a/src/components/FeaturePanel/helpers/getUrlForTag.tsx
+++ b/src/components/FeaturePanel/Properties/getUrlForTag.tsx
@@ -31,6 +31,10 @@ export const getUrlForTag = (k, v) => {
if (k === 'ref:edubase') {
return `https://get-information-schools.service.gov.uk/Establishments/Establishment/Details/${v}`;
}
+ if (k === 'gnis:feature_id') {
+ // alternative url in https://github.com/openstreetmap/id-tagging-schema/issues/272
+ return `https://edits.nationalmap.gov/apps/gaz-domestic/public/search/names/${v}`;
+ }
if (k === 'website') {
return v.match(urlRegExp) ? v : `http://${v}`;
}
diff --git a/src/components/FeaturePanel/Properties/helpers.tsx b/src/components/FeaturePanel/Properties/helpers.tsx
new file mode 100644
index 000000000..079841819
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/helpers.tsx
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+import Button from '@material-ui/core/Button';
+import ChevronRight from '@material-ui/icons/ChevronRight';
+import ExpandLessIcon from '@material-ui/icons/ExpandLess';
+import React from 'react';
+import { t } from '../../../services/intl';
+
+const StyledToggleButton = styled(Button)`
+ svg {
+ font-size: 17px;
+ }
+`;
+
+export const ShowMoreButton = ({ onClick, isShown }) => (
+
+ {!isShown && (
+ <>
+ {t('show_more')}
+ >
+ )}
+ {isShown && (
+ <>
+ {t('show_less')}
+ >
+ )}
+
+);
diff --git a/src/components/FeaturePanel/Properties/renderValue.tsx b/src/components/FeaturePanel/Properties/renderValue.tsx
new file mode 100644
index 000000000..01ec4d657
--- /dev/null
+++ b/src/components/FeaturePanel/Properties/renderValue.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { getUrlForTag } from './getUrlForTag';
+import { slashToOptionalBr } from '../../helpers';
+
+const getEllipsisHumanUrl = (humanUrl) => {
+ const MAX_LENGTH = 40;
+ return humanUrl.replace(/^([^/]+.{0,5})(.*)$/, (full, hostname, rest) => {
+ const charsLeft = MAX_LENGTH - 10 - hostname.length;
+ return (
+ hostname +
+ (full.length > MAX_LENGTH
+ ? `…${rest.substring(rest.length - charsLeft)}`
+ : rest)
+ );
+ });
+};
+
+const getHumanValue = (k, v, featured: boolean) => {
+ const humanValue = v.replace(/^https?:\/\//, '').replace(/^([^/]+)\/$/, '$1');
+
+ if (v.startsWith('https://commons.wikimedia.org/wiki/')) {
+ return v
+ .substring('https://commons.wikimedia.org/wiki/'.length)
+ .replace(/_/g, ' ');
+ }
+ if (k === 'image') {
+ return getEllipsisHumanUrl(humanValue);
+ }
+ if (k.match(/:?wikipedia$/) && v.match(/:/)) {
+ return v.split(':', 2)[1];
+ }
+ if (featured && k === 'wikidata') {
+ return `Wikipedia (wikidata)`; // TODO fetch label from wikidata
+ }
+ if (v === 'yes') {
+ return '✓';
+ }
+ if (v === 'no') {
+ return '✗';
+ }
+
+ return humanValue;
+};
+
+export const renderValue = (k, v, featured = false) => {
+ const url = getUrlForTag(k, v);
+ const humanValue = getHumanValue(k, v, featured);
+
+ return url ? {slashToOptionalBr(humanValue)} : humanValue;
+};
diff --git a/src/components/FeaturePanel/PublicTransport/LineNumber.tsx b/src/components/FeaturePanel/PublicTransport/LineNumber.tsx
new file mode 100644
index 000000000..c57733adf
--- /dev/null
+++ b/src/components/FeaturePanel/PublicTransport/LineNumber.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { useUserThemeContext } from '../../../helpers/theme';
+
+interface LineNumberProps {
+ name: string;
+ color: string;
+}
+
+/**
+ * A function to map a color name to hex. When a color is not found, it is returned as is, the same goes for hex colors.
+ * @param color The color to map to hex
+ * @returns The color in hex format, e.g. #ff0000
+ */
+function mapColorToHex(color: string) {
+ switch (color.toLowerCase()) {
+ case 'black':
+ return '#000000';
+ case 'gray':
+ case 'grey':
+ return '#808080';
+ case 'maroon':
+ return '#800000';
+ case 'olive':
+ return '#808000';
+ case 'green':
+ return '#008000';
+ case 'teal':
+ return '#008080';
+ case 'navy':
+ return '#000080';
+ case 'purple':
+ return '#800080';
+ case 'white':
+ return '#ffffff';
+ case 'silver':
+ return '#c0c0c0';
+ case 'red':
+ return '#ff0000';
+ case 'yellow':
+ return '#ffff00';
+ case 'lime':
+ return '#00ff00';
+ case 'aqua':
+ case 'cyan':
+ return '#00ffff';
+ case 'blue':
+ return '#0000ff';
+ case 'fuchsia':
+ case 'magenta':
+ return '#ff00ff';
+ default:
+ return color;
+ }
+}
+/**
+ * A function to determine whether the text color should be black or white
+ * @param hexBgColor The background color in hex format, e.g. #ff0000
+ * @returns 'black' or 'white' depending on the brightness of the background color
+ */
+function whiteOrBlackText(hexBgColor) {
+ const r = parseInt(hexBgColor.slice(1, 3), 16);
+ const g = parseInt(hexBgColor.slice(3, 5), 16);
+ const b = parseInt(hexBgColor.slice(5, 7), 16);
+ const brightness = (r * 299 + g * 587 + b * 114) / 1000;
+ return brightness > 125 ? 'black' : 'white';
+}
+
+export const LineNumber: React.FC = ({ name, color }) => {
+ const { currentTheme } = useUserThemeContext();
+ const darkmode = currentTheme === 'dark';
+
+ let bgcolor: string;
+ if (!color) bgcolor = darkmode ? '#898989' : '#dddddd';
+ // set the default color
+ else bgcolor = mapColorToHex(color);
+
+ const divStyle: React.CSSProperties = {
+ backgroundColor: bgcolor,
+ paddingBlock: '0.2rem',
+ paddingInline: '0.4rem',
+ borderRadius: '0.125rem',
+ display: 'inline',
+ color: whiteOrBlackText(bgcolor),
+ };
+
+ return {name}
;
+};
diff --git a/src/components/FeaturePanel/PublicTransport/PublicTransport.tsx b/src/components/FeaturePanel/PublicTransport/PublicTransport.tsx
new file mode 100644
index 000000000..ec9f3fd1c
--- /dev/null
+++ b/src/components/FeaturePanel/PublicTransport/PublicTransport.tsx
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from 'react';
+import { useRouter } from 'next/router';
+import { Typography } from '@material-ui/core';
+import { LineInformation, requestLines } from './requestRoutes';
+import { PublicTransportWrapper } from './PublicTransportWrapper';
+import { FeatureTags } from '../../../services/types';
+import { LineNumber } from './LineNumber';
+
+interface PublicTransportProps {
+ tags: FeatureTags;
+}
+
+const useLoadingState = () => {
+ const [routes, setRoutes] = useState([]);
+ const [error, setError] = useState();
+ const [loading, setLoading] = useState(true);
+
+ const finishRoutes = (payload) => {
+ setLoading(false);
+ setRoutes(payload);
+ };
+
+ const startRoutes = () => {
+ setLoading(true);
+ setRoutes([]);
+ setError(undefined);
+ };
+
+ const failRoutes = () => {
+ setError('Could not load routes');
+ setLoading(false);
+ };
+
+ return { routes, error, loading, startRoutes, finishRoutes, failRoutes };
+};
+
+const PublicTransportInner = () => {
+ const router = useRouter();
+
+ const { routes, error, loading, startRoutes, finishRoutes, failRoutes } =
+ useLoadingState();
+
+ useEffect(() => {
+ const loadData = async () => {
+ startRoutes();
+ const lines = await requestLines(
+ router.query.all[0] as any,
+ Number(router.query.all[1]),
+ ).catch(failRoutes);
+ finishRoutes(lines);
+ };
+
+ loadData();
+ }, []);
+
+ return (
+
+ {loading ? (
+ <>
+
.
+
.
+
.
+ >
+ ) : (
+
+ {routes.map((line) => (
+
+ ))}
+
+ )}
+ {error && (
+
+ Error: {error}
+
+ )}
+
+ );
+};
+
+export const PublicTransport: React.FC = ({ tags }) => {
+ const isPublicTransport =
+ Object.keys(tags).includes('public_transport') ||
+ tags.railway === 'station' ||
+ tags.railway === 'halt';
+
+ if (!isPublicTransport) {
+ return null;
+ }
+
+ return PublicTransportInner();
+};
diff --git a/src/components/FeaturePanel/PublicTransport/PublicTransportWrapper.tsx b/src/components/FeaturePanel/PublicTransport/PublicTransportWrapper.tsx
new file mode 100644
index 000000000..9d621eea6
--- /dev/null
+++ b/src/components/FeaturePanel/PublicTransport/PublicTransportWrapper.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export const PublicTransportWrapper = ({ children }) => {
+ const divStyle: React.CSSProperties = {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'start',
+ justifyContent: 'start',
+ gap: '0.5rem',
+ flexWrap: 'wrap',
+ };
+
+ return {children}
;
+};
diff --git a/src/components/FeaturePanel/PublicTransport/requestRoutes.ts b/src/components/FeaturePanel/PublicTransport/requestRoutes.ts
new file mode 100644
index 000000000..fb21ac6e0
--- /dev/null
+++ b/src/components/FeaturePanel/PublicTransport/requestRoutes.ts
@@ -0,0 +1,43 @@
+import { fetchText } from '../../../services/fetch';
+
+export interface LineInformation {
+ ref: string;
+ colour: string | undefined;
+}
+
+export async function requestLines(
+ featureType: 'node' | 'way' | 'relation',
+ id: number,
+) {
+ // use the overpass api to request the lines in
+ const overpassQuery = `[out:csv(ref, colour; false; ';')];
+ ${featureType}(${id});
+ rel(bn)["public_transport"="stop_area"];
+node(r: "stop") -> .stops;
+ rel(bn.stops)["route"~"bus|train|tram|subway|light_rail|ferry|monorail"];
+ out;`;
+
+ // send the request
+ const response: string = await fetchText(
+ `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
+ overpassQuery,
+ )}`,
+ );
+
+ let resData = response.split('\n').map((line) => {
+ const ref = line.split(';').slice(0, -1).join(';');
+ let colour = line.split(';')[line.split(';').length - 1];
+
+ // set colour to undefined if it is empty
+ if (colour === '') colour = undefined;
+ return { ref, colour } as LineInformation;
+ });
+
+ resData = resData.filter((line) => line.ref !== '');
+ // remove duplicats
+ resData = resData.filter(
+ (line, index) => resData.findIndex((l) => l.ref === line.ref) === index,
+ );
+ resData.sort((a, b) => a.ref.localeCompare(b.ref));
+ return resData;
+}
diff --git a/src/components/FeaturePanel/TagsTable.tsx b/src/components/FeaturePanel/TagsTable.tsx
deleted file mode 100644
index 6bbefb7bc..000000000
--- a/src/components/FeaturePanel/TagsTable.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import truncate from 'lodash/truncate';
-
-import { slashToOptionalBr, useToggleState } from '../helpers';
-import { getUrlForTag } from './helpers/getUrlForTag';
-import { EditIconButton } from './helpers/EditIconButton';
-import { buildAddress } from '../../services/helpers';
-import { ToggleButton } from './helpers/ToggleButton';
-
-const Wrapper = styled.div`
- position: relative;
- margin-bottom: 2em;
-`;
-
-const Table = styled.table`
- font-size: 1rem;
- width: 100%;
-
- th,
- td {
- padding: 0.1em;
- overflow: hidden;
-
- &:hover .show-on-hover {
- display: block !important;
- }
- }
-
- th {
- width: 140px;
- max-width: 140px;
- color: ${({ theme }) => theme.palette.text.secondary};
- text-align: left;
- font-weight: normal;
- vertical-align: baseline;
- padding-left: 0;
- }
-
- table {
- padding-left: 1em;
- padding-bottom: 1em;
- }
-`;
-
-const renderValue = (k, v) => {
- const url = getUrlForTag(k, v);
- let humanUrl = v.replace(/^https?:\/\//, '').replace(/^([^/]+)\/$/, '$1');
- if (k === 'image') {
- humanUrl = humanUrl.replace(/^([^/]+.{0,5})(.*)$/, (full, p1, p2) => {
- const charsLeft = 30 - p1.length;
- return (
- p1 + (full.length > 40 ? `…${p2.substring(p2.length - charsLeft)}` : p2)
- );
- });
- }
- return url ? {slashToOptionalBr(humanUrl)} : v;
-};
-
-const isAddr = (k) => k.match(/^addr:|uir_adr|:addr/);
-const isName = (k) => k.match(/^name(:|$)/);
-const isShortName = (k) => k.match(/^short_name(:|$)/);
-const isAltName = (k) => k.match(/^alt_name(:|$)/);
-const isOfficialName = (k) => k.match(/^official_name(:|$)/);
-const isOldName = (k) => k.match(/^old_name(:|$)/);
-const isBuilding = (k) =>
- k.match(/building|roof|^min_level|^max_level|height$/);
-const isNetwork = (k) => k.match(/network/);
-const isBrand = (k) => k.match(/^brand/);
-const isPayment = (k) => k.match(/^payment/);
-
-const TagsGroup = ({ tags, label, value, hideArrow = false, onEdit }) => {
- const [isShown, toggle] = useToggleState(false);
-
- if (!tags.length) {
- return null;
- }
-
- return (
- <>
-
- {label} |
-
- onEdit(tags[0][0])} />
- {value || tags[0]?.join(' = ')}
- {!hideArrow && }
- |
-
- {isShown && (
-
-
-
-
- {tags.map(([k, v]) => (
-
- {k} |
- {renderValue(k, v)} |
-
- ))}
-
-
- |
-
- )}
- >
- );
-};
-
-// This is supposed to be replaced by iD presets in future (see https://github.com/zbycz/osmapp/issues/116)
-export const TagsTable = ({ tags, center, except, onEdit }) => {
- const tagsEntries = Object.entries(tags).filter(([k]) => !except.includes(k));
-
- const addrs = tagsEntries.filter(([k]) => isAddr(k));
- const names = tagsEntries.filter(([k]) => isName(k));
- const shortNames = tagsEntries.filter(([k]) => isShortName(k));
- const altNames = tagsEntries.filter(([k]) => isAltName(k));
- const officialNames = tagsEntries.filter(([k]) => isOfficialName(k));
- const oldNames = tagsEntries.filter(([k]) => isOldName(k));
- const buildings = tagsEntries.filter(([k]) => isBuilding(k));
- const networks = tagsEntries.filter(([k]) => isNetwork(k));
- const brands = tagsEntries.filter(([k]) => isBrand(k));
- const payments = tagsEntries.filter(([k]) => isPayment(k));
- const rest = tagsEntries.filter(
- ([k]) =>
- !isName(k) &&
- !isShortName(k) &&
- !isAltName(k) &&
- !isOfficialName(k) &&
- !isOldName(k) &&
- !isAddr(k) &&
- !isBuilding(k) &&
- !isNetwork(k) &&
- !isPayment(k) &&
- !isBrand(k),
- );
-
- return (
-
-
-
-
-
-
-
-
-
- {rest.map(([k, v]) => (
-
- {k} |
-
- onEdit(k)} />
- {renderValue(k, v)}
- |
-
- ))}
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/FeaturePanel/helpers/EditDialogContext.tsx b/src/components/FeaturePanel/helpers/EditDialogContext.tsx
new file mode 100644
index 000000000..809efbcfb
--- /dev/null
+++ b/src/components/FeaturePanel/helpers/EditDialogContext.tsx
@@ -0,0 +1,36 @@
+import React, { createContext, useContext, useState } from 'react';
+import Router from 'next/router';
+import { isBrowser } from '../../helpers';
+
+interface EditDialogType {
+ opened: boolean;
+ focusTag: string | boolean;
+ open: () => void;
+ close: () => void;
+ openWithTag: (tag: string) => void;
+}
+
+export const EditDialogContext = createContext(undefined);
+
+// lives in App.tsx because it needs ctx in SSR
+export const EditDialogProvider = ({ children }) => {
+ const initialState = isBrowser() ? Router.query.all?.[2] === 'edit' : false; // TODO supply router.query in SSR
+ const [openedWithTag, setOpenedWithTag] =
+ useState(initialState);
+
+ const value = {
+ opened: !!openedWithTag,
+ focusTag: openedWithTag,
+ open: () => setOpenedWithTag(true),
+ close: () => setOpenedWithTag(false),
+ openWithTag: setOpenedWithTag,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useEditDialogContext = () => useContext(EditDialogContext);
diff --git a/src/components/FeaturePanel/helpers/Subheading.tsx b/src/components/FeaturePanel/helpers/Subheading.tsx
new file mode 100644
index 000000000..8c3b5f90c
--- /dev/null
+++ b/src/components/FeaturePanel/helpers/Subheading.tsx
@@ -0,0 +1,8 @@
+import { Typography } from '@material-ui/core';
+import React from 'react';
+
+export const Subheading = ({ children }) => (
+
+ {children}
+
+);
diff --git a/src/components/FeaturePanel/renderers/FoodHygieneRatingScheme.tsx b/src/components/FeaturePanel/renderers/FoodHygieneRatingScheme.tsx
new file mode 100644
index 000000000..e0021230d
--- /dev/null
+++ b/src/components/FeaturePanel/renderers/FoodHygieneRatingScheme.tsx
@@ -0,0 +1,105 @@
+import React, { useEffect, useState } from 'react';
+import { Tooltip, Typography } from '@material-ui/core';
+import RestaurantIcon from '@material-ui/icons/Restaurant';
+import styled from 'styled-components';
+import { getEstablishmentRatingValue } from '../../../services/fhrsApi';
+
+const useLoadingState = () => {
+ const [rating, setRating] = useState();
+ const [error, setError] = useState();
+ const [loading, setLoading] = useState(true);
+
+ const finishRating = (payload) => {
+ setLoading(false);
+ setRating(payload);
+ };
+
+ const startRating = () => {
+ setLoading(true);
+ setRating(undefined);
+ setError(undefined);
+ };
+
+ const failRating = () => {
+ setError('Could not load rating');
+ setLoading(false);
+ };
+
+ return { rating, error, loading, startRating, finishRating, failRating };
+};
+
+const Wrapper = styled.div`
+ display: flex;
+ gap: 0.5rem;
+
+ .MuiRating-root {
+ margin-top: -2px;
+ }
+`;
+
+const RatingRound = styled.span`
+ border-radius: 50%;
+ background-color: #1a6500;
+ color: #fff;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ font-weight: bold;
+ position: relative;
+ top: -1px;
+`;
+
+export const FoodHygieneRatingSchemeRenderer = ({ v }) => {
+ const { rating, error, loading, startRating, finishRating, failRating } =
+ useLoadingState();
+
+ useEffect(() => {
+ const loadData = async () => {
+ startRating();
+ const ratingValue = await getEstablishmentRatingValue(v);
+ if (Number.isNaN(rating)) {
+ failRating();
+ }
+ finishRating(ratingValue);
+ };
+
+ loadData();
+ }, []);
+
+ if (loading) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ FHRS{' '}
+ {Number.isNaN(rating) || error ? (
+
+ (Error while fetching rating)
+
+ ) : (
+ {rating}
+ )}
+
+
+
+ >
+ );
+};
diff --git a/src/components/FeaturePanel/renderers/PhoneRenderer.tsx b/src/components/FeaturePanel/renderers/PhoneRenderer.tsx
index 9dfa49db8..6b4850be3 100644
--- a/src/components/FeaturePanel/renderers/PhoneRenderer.tsx
+++ b/src/components/FeaturePanel/renderers/PhoneRenderer.tsx
@@ -4,8 +4,14 @@ import LocalPhone from '@material-ui/icons/LocalPhone';
export const WebsiteRenderer = ({ v }) => (
<>
-
- {v}
+
+ {v.split(';').map((v2, index) => (
+ <>
+ {index === 0 ? '' : ', '}
+ {v2}
+ >
+ ))}
+
>
);
diff --git a/src/components/FeaturePanel/renderers/WikidataRenderer.tsx b/src/components/FeaturePanel/renderers/WikidataRenderer.tsx
new file mode 100644
index 000000000..c14731833
--- /dev/null
+++ b/src/components/FeaturePanel/renderers/WikidataRenderer.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { renderValue } from '../Properties/renderValue';
+import { WikipediaIcon } from '../../../assets/WikipediaIcon';
+
+export const WikidataRenderer = ({ k, v }) => (
+ <>
+
+ {renderValue(k, v, true)}
+ >
+);
diff --git a/src/components/FeaturePanel/renderers/WikipediaRenderer.tsx b/src/components/FeaturePanel/renderers/WikipediaRenderer.tsx
new file mode 100644
index 000000000..ed5798496
--- /dev/null
+++ b/src/components/FeaturePanel/renderers/WikipediaRenderer.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { WikipediaIcon } from '../../../assets/WikipediaIcon';
+import { renderValue } from '../Properties/renderValue';
+
+export const WikipediaRenderer = ({ k, v }) => (
+ <>
+
+ {renderValue(k, v)}
+ >
+);
diff --git a/src/components/HomepagePanel/HomepagePanel.tsx b/src/components/HomepagePanel/HomepagePanel.tsx
index 503176133..3d7b0ebb0 100644
--- a/src/components/HomepagePanel/HomepagePanel.tsx
+++ b/src/components/HomepagePanel/HomepagePanel.tsx
@@ -156,16 +156,14 @@ export const HomepagePanel = () => {
-
- }
- color="primary"
- href="/install"
- >
- {t('install.button')}
-
-
+ }
+ color="primary"
+ href="/install"
+ >
+ {t('install.button')}
+
diff --git a/src/components/HomepagePanel/InstallDialog.tsx b/src/components/HomepagePanel/InstallDialog.tsx
index e6e299114..54c2c694c 100644
--- a/src/components/HomepagePanel/InstallDialog.tsx
+++ b/src/components/HomepagePanel/InstallDialog.tsx
@@ -140,25 +140,25 @@ export function InstallDialog() {
{' '}
-
+
{' '}
-
+
@@ -178,12 +178,12 @@ export function InstallDialog() {
-
+
-
+
@@ -203,13 +203,13 @@ export function InstallDialog() {
{' '}
-
+
diff --git a/src/components/LayerSwitcher/LayerSwitcherContent.tsx b/src/components/LayerSwitcher/LayerSwitcherContent.tsx
index b5802358b..00632a81b 100644
--- a/src/components/LayerSwitcher/LayerSwitcherContent.tsx
+++ b/src/components/LayerSwitcher/LayerSwitcherContent.tsx
@@ -13,8 +13,9 @@ import {
RemoveUserLayerAction,
} from './helpers';
import { osmappLayers } from './osmappLayers';
-import { Layer, useMapStateContext } from '../utils/MapStateContext';
+import { Layer, useMapStateContext, View } from '../utils/MapStateContext';
import { usePersistedState } from '../utils/usePersistedState';
+import { isViewInsideAfrica } from '../Map/styles/makinaAfricaStyle';
const StyledList = styled(List)`
.MuiListItemIcon-root {
@@ -36,14 +37,16 @@ const Spacer = styled.div`
padding-bottom: 1.5em;
`;
-const getAllLayers = (userLayers: Layer[]): Layer[] => {
- const spacer = { type: 'spacer' as const, key: 'userSpacer' };
+const getAllLayers = (userLayers: Layer[], view: View): Layer[] => {
+ const spacer: Layer = { type: 'spacer' as const, key: 'userSpacer' };
+ const toLayer = ([key, layer]) => ({ ...layer, key });
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const filterMakina = ([key, _]) =>
+ key === 'makinaAfrica' ? isViewInsideAfrica(view) : true; // needs suppressHydrationWarning
return [
- ...Object.entries(osmappLayers).map(([key, layer]) => ({
- ...layer,
- key,
- })),
+ ...Object.entries(osmappLayers).filter(filterMakina).map(toLayer),
...(userLayers.length ? [spacer] : []),
...userLayers.map((layer) => ({
...layer,
@@ -55,15 +58,19 @@ const getAllLayers = (userLayers: Layer[]): Layer[] => {
};
export const LayerSwitcherContent = () => {
- const { activeLayers, setActiveLayers } = useMapStateContext();
+ const { view, activeLayers, setActiveLayers } = useMapStateContext();
const [userLayers, setUserLayers] = usePersistedState('userLayers', []);
- const layers = getAllLayers(userLayers);
+ const layers = getAllLayers(userLayers, view);
return (
<>
-
+
{layers.map(({ key, name, type, url, Icon }) => {
if (type === 'spacer') {
return ;
diff --git a/src/components/LayerSwitcher/osmappLayers.tsx b/src/components/LayerSwitcher/osmappLayers.tsx
index 55421ad7a..d392d2c29 100644
--- a/src/components/LayerSwitcher/osmappLayers.tsx
+++ b/src/components/LayerSwitcher/osmappLayers.tsx
@@ -5,28 +5,59 @@ import SatelliteIcon from '@material-ui/icons/Satellite';
import DirectionsBikeIcon from '@material-ui/icons/DirectionsBike';
import { Layer } from '../utils/MapStateContext';
import { t } from '../../services/intl';
+import { isBrowser } from '../helpers';
interface Layers {
[key: string]: Layer;
}
-const retina = (window.devicePixelRatio || 1) >= 2 ? '@2x' : '';
+const retina =
+ ((isBrowser() && window.devicePixelRatio) || 1) >= 2 ? '@2x' : '';
export const osmappLayers: Layers = {
- basic: { name: t('layers.basic'), type: 'basemap', Icon: ExploreIcon },
- outdoor: { name: t('layers.outdoor'), type: 'basemap', Icon: FilterHdrIcon },
+ basic: {
+ name: t('layers.basic'),
+ type: 'basemap',
+ Icon: ExploreIcon,
+ attribution: ['maptiler', 'osm'],
+ },
+ makinaAfrica: {
+ name: t('layers.makina_africa'),
+ type: 'basemap',
+ Icon: ExploreIcon,
+ attribution: [
+ 'OPG © OpenMapTiles',
+ 'osm',
+ ],
+ },
+ outdoor: {
+ name: t('layers.outdoor'),
+ type: 'basemap',
+ Icon: FilterHdrIcon,
+ attribution: ['maptiler', 'osm'],
+ },
s1: { type: 'spacer' },
mapnik: {
name: t('layers.mapnik'),
type: 'basemap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
Icon: MapIcon,
+ attribution: ['osm'],
},
sat: {
- name: t('layers.sat'),
+ name: t('layers.maptilerSat'),
type: 'basemap',
- url: 'https://api.maptiler.com/tiles/satellite/tiles.json?key=7dlhLl3hiXQ1gsth0kGu',
+ url: 'https://api.maptiler.com/tiles/satellite-v2/tiles.json?key=7dlhLl3hiXQ1gsth0kGu',
Icon: SatelliteIcon,
+ attribution: ['maptiler'],
+ },
+ bingSat: {
+ name: t('layers.bingSat'),
+ type: 'basemap',
+ url: 'https://ecn.{bingSubdomains}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=13657',
+ Icon: SatelliteIcon,
+ attribution: ['© Microsoft'],
+ maxzoom: 19,
},
// mtb: {
// name: t('layers.mtb'),
@@ -36,12 +67,16 @@ export const osmappLayers: Layers = {
bike: {
name: t('layers.bike'),
type: 'basemap',
- url: `https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}${retina}.png?apikey=00291b657a5d4c91bbacb0ff096e2c25`,
+ url: `https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}${retina}.png?apikey=18c0cb31f2fd41d28ac90abe4059e359`,
Icon: DirectionsBikeIcon,
+ attribution: [
+ '© Thunderforest',
+ 'osm',
+ ],
},
// snow: {
// name: t('layers.snow'),
- // type: 'basemap',
+ // type: 'overlay',
// url: 'https://www.opensnowmap.org/tiles-pistes/{z}/{x}/{y}.png',
// },
// s2: { type: 'spacer' },
diff --git a/src/components/Map/BrowserMap.tsx b/src/components/Map/BrowserMap.tsx
index ad430d5ec..10403ac83 100644
--- a/src/components/Map/BrowserMap.tsx
+++ b/src/components/Map/BrowserMap.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import 'maplibre-gl/dist/maplibre-gl.css';
-import maplibregl from 'maplibre-gl';
import { useAddMapEvent, useMapEffect, useMobileMode } from '../helpers';
import { useMapStateContext } from '../utils/MapStateContext';
import { useFeatureContext } from '../utils/FeatureContext';
@@ -10,6 +9,8 @@ import { useUpdateViewOnMove } from './behaviour/useUpdateViewOnMove';
import { useUpdateStyle } from './behaviour/useUpdateStyle';
import { useInitMap } from './behaviour/useInitMap';
import { Translation } from '../../services/intl';
+import { useToggleTerrainControl } from './behaviour/useToggleTerrainControl';
+import { isWebglSupported } from './helpers';
const useOnMapLoaded = useAddMapEvent((map, onMapLoaded) => ({
eventType: 'load',
@@ -32,7 +33,7 @@ const NotSupportedMessage = () => (
// TODO https://cdn.klokantech.com/openmaptiles-language/v1.0/openmaptiles-language.js + use localized name in FeaturePanel
const BrowserMap = ({ onMapLoaded }) => {
- if (!maplibregl.supported()) {
+ if (!isWebglSupported()) {
onMapLoaded();
return ;
}
@@ -46,6 +47,7 @@ const BrowserMap = ({ onMapLoaded }) => {
const { viewForMap, setViewFromMap, setBbox, activeLayers } =
useMapStateContext();
useUpdateViewOnMove(map, setViewFromMap, setBbox);
+ useToggleTerrainControl(map);
useUpdateMap(map, viewForMap);
useUpdateStyle(map, activeLayers);
diff --git a/src/components/Map/MapFooter/LangSwitcher.tsx b/src/components/Map/MapFooter/LangSwitcher.tsx
index 1fe8014bb..96a859868 100644
--- a/src/components/Map/MapFooter/LangSwitcher.tsx
+++ b/src/components/Map/MapFooter/LangSwitcher.tsx
@@ -1,6 +1,7 @@
import getConfig from 'next/config';
import React from 'react';
import { Menu, MenuItem } from '@material-ui/core';
+import { useRouter } from 'next/router';
import { useBoolState } from '../../helpers';
import { changeLang, intl, t } from '../../../services/intl';
@@ -8,15 +9,16 @@ export const LangSwitcher = () => {
const {
publicRuntimeConfig: { languages },
} = getConfig();
+ const { asPath } = useRouter();
const anchorRef = React.useRef();
const [opened, open, close] = useBoolState(false);
- const setLang = (k) => {
- changeLang(k);
+ const getLangSetter = (lang) => (e) => {
+ e.preventDefault();
+ changeLang(lang);
close();
};
- // TODO make a link and allow google to index all langs
return (
<>