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 ( + + + {t('user.login')} + + ); + } + + return ( + <> + + + {osmUser} + + + {t('user.logout')} + + ); +}); + // 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 ( <> + + + - - + - + + ); }; 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}`],