diff --git a/src/components/LayerSwitcher/LayerSwitcherButton.tsx b/src/components/LayerSwitcher/LayerSwitcherButton.tsx
index 3ce0442d..6839349c 100644
--- a/src/components/LayerSwitcher/LayerSwitcherButton.tsx
+++ b/src/components/LayerSwitcher/LayerSwitcherButton.tsx
@@ -2,19 +2,6 @@ import React from 'react';
import styled from 'styled-components';
import LayersIcon from './LayersIcon';
import { t } from '../../services/intl';
-import { isDesktop } from '../helpers';
-
-const TopRight = styled.div`
- position: absolute;
- z-index: 1000;
- padding: 10px;
- right: 0;
- top: 72px;
-
- @media ${isDesktop} {
- top: 0;
- }
-`;
const StyledLayerSwitcher = styled.button`
margin: 0;
@@ -41,10 +28,8 @@ const StyledLayerSwitcher = styled.button`
`;
export const LayerSwitcherButton = ({ onClick }: { onClick?: any }) => (
-
-
-
- {t('layerswitcher.button')}
-
-
+
+
+ {t('layerswitcher.button')}
+
);
diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx
index 24285649..71185701 100644
--- a/src/components/Map/Map.tsx
+++ b/src/components/Map/Map.tsx
@@ -5,21 +5,25 @@ import dynamic from 'next/dynamic';
import BugReport from '@material-ui/icons/BugReport';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
-import { useBoolState } from '../helpers';
+import { isDesktop, useBoolState } from '../helpers';
import { MapFooter } from './MapFooter/MapFooter';
import { SHOW_PROTOTYPE_UI } from '../../config';
import { LayerSwitcherButton } from '../LayerSwitcher/LayerSwitcherButton';
import { MaptilerLogo } from './MapFooter/MaptilerLogo';
+import { TopMenu } from './TopMenu/TopMenu';
const BrowserMap = dynamic(() => import('./BrowserMap'), {
ssr: false,
loading: () =>
,
});
-const LayerSwitcher = dynamic(() => import('../LayerSwitcher/LayerSwitcher'), {
- ssr: false,
- loading: () => ,
-});
+const LayerSwitcherDynamic = dynamic(
+ () => import('../LayerSwitcher/LayerSwitcher'),
+ {
+ ssr: false,
+ loading: () => ,
+ },
+);
const Spinner = styled(CircularProgress)`
position: absolute;
@@ -28,6 +32,18 @@ const Spinner = styled(CircularProgress)`
margin: -20px 0 0 -20px;
`;
+const TopRight = styled.div`
+ position: absolute;
+ z-index: 1000;
+ padding: 10px;
+ right: 0;
+ top: 72px;
+
+ @media ${isDesktop} {
+ top: 0;
+ }
+`;
+
const BottomRight = styled.div`
position: absolute;
right: 0;
@@ -60,7 +76,10 @@ const Map = () => {
{!mapLoaded && }
-
+
+
+
+
{SHOW_PROTOTYPE_UI && }
diff --git a/src/components/Map/MapFooter/MapFooter.tsx b/src/components/Map/MapFooter/MapFooter.tsx
index 14f99dd7..b8fcb3f1 100644
--- a/src/components/Map/MapFooter/MapFooter.tsx
+++ b/src/components/Map/MapFooter/MapFooter.tsx
@@ -5,7 +5,6 @@ import { Tooltip, useMediaQuery } from '@material-ui/core';
import uniq from 'lodash/uniq';
import { t, Translation } from '../../../services/intl';
import GithubIcon from '../../../assets/GithubIcon';
-import { MoreMenu } from './MoreMenu';
import { LangSwitcher } from './LangSwitcher';
import { useMapStateContext } from '../../utils/MapStateContext';
import { osmappLayers } from '../../LayerSwitcher/osmappLayers';
@@ -128,8 +127,6 @@ export const MapFooter = () => (
{' | '}
- {' | '}
-
);
diff --git a/src/components/Map/MapFooter/MoreMenu.tsx b/src/components/Map/TopMenu/HamburgerMenu.tsx
similarity index 72%
rename from src/components/Map/MapFooter/MoreMenu.tsx
rename to src/components/Map/TopMenu/HamburgerMenu.tsx
index d0440b4b..31c24c8a 100644
--- a/src/components/Map/MapFooter/MoreMenu.tsx
+++ b/src/components/Map/TopMenu/HamburgerMenu.tsx
@@ -1,26 +1,24 @@
-import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import BrightnessAutoIcon from '@material-ui/icons/BrightnessAuto';
import Brightness4Icon from '@material-ui/icons/Brightness4';
import BrightnessHighIcon from '@material-ui/icons/BrightnessHigh';
-import React, { useEffect, useState } from 'react';
+import React, { forwardRef, useEffect, useState } from 'react';
import { Menu, MenuItem } from '@material-ui/core';
import CreateIcon from '@material-ui/icons/Create';
import HelpIcon from '@material-ui/icons/Help';
import styled from 'styled-components';
import GetAppIcon from '@material-ui/icons/GetApp';
import Router from 'next/router';
+import IconButton from '@material-ui/core/IconButton';
+import MenuIcon from '@material-ui/icons/Menu';
+import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import { useBoolState } from '../../helpers';
import { t } from '../../../services/intl';
import { useFeatureContext } from '../../utils/FeatureContext';
import { useMapStateContext } from '../../utils/MapStateContext';
import { getIdEditorLink } from '../../../utils';
import { useUserThemeContext } from '../../../helpers/theme';
-
-const StyledChevronRightIcon = styled(ChevronRightIcon)`
- color: ${({ theme }) => theme.palette.tertiary.main};
- margin: -2px 0px -2px -1px !important;
- font-size: 15px !important;
-`;
+import { useOsmAuthContext } from '../../utils/OsmAuthContext';
+import { LoginIcon } from './LoginIcon';
const PencilIcon = styled(CreateIcon)`
color: ${({ theme }) => theme.palette.action.active};
@@ -58,6 +56,16 @@ const StyledBrightnessHighIcon = styled(BrightnessHighIcon)`
font-size: 17px !important;
`;
+const StyledAccountCircleIcon = styled(AccountCircleIcon)`
+ color: ${({ theme }) => theme.palette.action.active};
+ margin: -2px 6px 0 0;
+ font-size: 17px !important;
+`;
+
+const StyledDivider = styled.hr`
+ border-top: 1px solid ${({ theme }) => theme.palette.divider};
+`;
+
const useIsBrowser = () => {
// fixes hydration error - server and browser have different view (cookies and window.hash)
// throwed "Warning: Prop `href` did not match."
@@ -146,6 +154,36 @@ const ThemeSelection = () => {
);
};
+const UserLogin = forwardRef(({ closeMenu }, ref) => {
+ const { osmUser, handleLogin, handleLogout } = useOsmAuthContext();
+
+ if (!osmUser) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ >
+ );
+});
+
// TODO maybe
//
//
@@ -157,35 +195,41 @@ const ThemeSelection = () => {
// https://github.com/mui-org/material-ui/issues/22912
// https://github.com/mui-org/material-ui/issues?q=is%3Aissue+is%3Aopen+menuitem+keyboard
-export const MoreMenu = () => {
+export const HamburgerMenu = () => {
const anchorRef = React.useRef();
const [opened, open, close] = useBoolState(false);
return (
<>
-
+
- {t('map.more_button')}
-
-
+
+
>
);
};
diff --git a/src/components/Map/TopMenu/LoginIcon.tsx b/src/components/Map/TopMenu/LoginIcon.tsx
new file mode 100644
index 00000000..7cc03126
--- /dev/null
+++ b/src/components/Map/TopMenu/LoginIcon.tsx
@@ -0,0 +1,24 @@
+import IconButton from '@material-ui/core/IconButton';
+import AccountCircleIcon from '@material-ui/icons/AccountCircle';
+import React from 'react';
+import styled from 'styled-components';
+import { useOsmAuthContext } from '../../utils/OsmAuthContext';
+
+const StyledUserImg = styled.img`
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+`;
+export const LoginIcon = ({ onClick }) => {
+ const { osmUser, userImage } = useOsmAuthContext();
+
+ return (
+
+ {osmUser ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/src/components/Map/TopMenu/TopMenu.tsx b/src/components/Map/TopMenu/TopMenu.tsx
new file mode 100644
index 00000000..4369e44f
--- /dev/null
+++ b/src/components/Map/TopMenu/TopMenu.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import styled from 'styled-components';
+import { isDesktop } from '../../helpers';
+import { HamburgerMenu } from './HamburgerMenu';
+
+const Wrapper = styled.span`
+ vertical-align: top;
+ display: inline-block;
+
+ margin-top: -10px;
+
+ @media ${isDesktop} {
+ margin-top: -5px;
+ }
+
+ button:last-child {
+ margin-left: -10px;
+ }
+
+ svg {
+ filter: drop-shadow(0 0 2px #ffffff);
+ }
+`;
+
+export const TopMenu = () => (
+
+
+
+);
diff --git a/src/components/utils/OsmAuthContext.tsx b/src/components/utils/OsmAuthContext.tsx
index 2d6a8d64..945f6a3d 100644
--- a/src/components/utils/OsmAuthContext.tsx
+++ b/src/components/utils/OsmAuthContext.tsx
@@ -1,13 +1,16 @@
import React, { createContext, useContext, useState } from 'react';
import {
- fetchOsmUsername,
- getOsmUsername,
+ fetchOsmUser,
+ getOsmUser,
osmLogout,
+ OsmUser,
} from '../../services/osmApiAuth';
+import { useMapStateContext } from './MapStateContext';
interface OsmAuthType {
loggedIn: boolean;
osmUser: string;
+ userImage: string;
handleLogin: () => void;
handleLogout: () => void;
}
@@ -15,13 +18,24 @@ interface OsmAuthType {
export const OsmAuthContext = createContext(undefined);
export const OsmAuthProvider = ({ children }) => {
- const [osmUser, setOsmUser] = useState(getOsmUsername() ?? '');
- const handleLogin = () => fetchOsmUsername().then(setOsmUser);
- const handleLogout = () => osmLogout().then(() => setOsmUser(''));
+ const mapStateContext = useMapStateContext();
+ const [osmUser, setOsmUser] = useState(getOsmUser());
+
+ const successfulLogin = (user: OsmUser) => {
+ setOsmUser(user);
+ mapStateContext.showToast({
+ content: `Logged in as ${user.name}`,
+ type: 'success',
+ });
+ };
+
+ const handleLogin = () => fetchOsmUser().then(successfulLogin);
+ const handleLogout = () => osmLogout().then(() => setOsmUser(undefined));
const value = {
loggedIn: !!osmUser,
- osmUser,
+ osmUser: osmUser?.name || '', // TODO rename
+ userImage: osmUser?.imageUrl || '',
handleLogin,
handleLogout,
};
diff --git a/src/locales/cs.js b/src/locales/cs.js
index fb2583d2..d8df6e61 100644
--- a/src/locales/cs.js
+++ b/src/locales/cs.js
@@ -9,6 +9,9 @@ export default {
show_more: 'Zobrazit více',
show_less: 'Zobrazit méně',
+ 'user.login': 'Přihlásit se',
+ 'user.logout': 'Odhlásit se',
+
'project.osmapp.description': 'Univerzální appka pro OpenStreetMap',
'project.osmapp.serpDescription': 'Otevřená mapa světa nad OpenStreetMap databází. Hledání, klikatelné POIs, editace a více!',
@@ -95,7 +98,7 @@ export default {
'(c) MapTiler.com ❤️
– vektorové dlaždice, hosting, turistická mapa
Velký dík za podporu tohoto projektu! 🙂 ',
'map.more_button': 'více',
'map.more_button_title': 'Další možnosti…',
- 'map.edit_link': 'Otevřít oblast v editoru iD',
+ 'map.edit_link': 'Otevřít mapu v editoru iD',
'map.about_link': 'O aplikaci',
'editdialog.add_heading': 'Přidat do OpenStreetMap',
diff --git a/src/locales/vocabulary.js b/src/locales/vocabulary.js
index a7fbbcc7..2cb98e23 100644
--- a/src/locales/vocabulary.js
+++ b/src/locales/vocabulary.js
@@ -14,6 +14,9 @@ export default {
show_more: 'Show more',
show_less: 'Show less',
+ 'user.login': 'Login',
+ 'user.logout': 'Logout',
+
'project.osmapp.description': 'A universal app for OpenStreetMap',
'project.osmapp.serpDescription':
'An open-source map of the world based on the OpenStreetMap database. Features a search, clickable points of interest, in-app map edits, and more!',
diff --git a/src/services/osmApiAuth.ts b/src/services/osmApiAuth.ts
index 8303556d..a4f74822 100644
--- a/src/services/osmApiAuth.ts
+++ b/src/services/osmApiAuth.ts
@@ -46,23 +46,38 @@ const authFetch = async (options) =>
});
});
-export const fetchOsmUsername = async () => {
- const details = await authFetch({
+export type OsmUser = {
+ name: string;
+ imageUrl: string;
+};
+
+export const fetchOsmUser = async (): Promise => {
+ const response = await authFetch({
method: 'GET',
path: '/api/0.6/user/details.json',
});
- const name = JSON.parse(details).user.display_name;
- window.localStorage.setItem('osm_username', name);
- return name;
+ const details = JSON.parse(response).user;
+ const user = {
+ name: details.display_name,
+ imageUrl:
+ details.img?.href ??
+ `https://www.gravatar.com/avatar/${details.id}?s=24&d=robohash`,
+ };
+
+ window.localStorage.setItem('osm_user', JSON.stringify(user));
+ return user;
};
+export const getOsmUser = (): OsmUser | undefined =>
+ auth.authenticated()
+ ? JSON.parse(window.localStorage.getItem('osm_user'))
+ : undefined;
+
export const osmLogout = async () => {
auth.logout();
+ window.localStorage.removeItem('osm_user');
};
-export const getOsmUsername = () =>
- auth.authenticated() && window.localStorage.getItem('osm_username');
-
const getChangesetXml = ({ changesetComment, feature }) => {
const tags = [
['created_by', `OsmAPP ${osmappVersion}`],