From 7c8028e0480273b5c606e1d0caf961b7212f77bd Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:42:55 +0200 Subject: [PATCH 1/3] Map: Make airports clickable (#518) --- src/components/Map/helpers.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Map/helpers.ts b/src/components/Map/helpers.ts index 3ed4ccca..800a9eb0 100644 --- a/src/components/Map/helpers.ts +++ b/src/components/Map/helpers.ts @@ -2,11 +2,18 @@ import { OsmId } from '../../services/types'; import { isBrowser } from '../helpers'; import { getGlobalMap } from '../../services/mapStorage'; -const isOsmLayer = (id) => { +const isOsmLayer = (id: string) => { if (id.startsWith('place-country-')) return false; // https://github.com/zbycz/osmapp/issues/35 if (id === 'place-continent') return false; if (id === 'water-name-ocean') return false; - const prefixes = ['water-name-', 'poi-', 'place-', 'overpass-', 'climbing-']; + const prefixes = [ + 'water-name-', + 'poi-', + 'place-', + 'overpass-', + 'climbing-', + 'airport-', + ]; return prefixes.some((prefix) => id.startsWith(prefix)); }; From a8b019b5c2213e167bace51dc511fcedd172d18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Sat, 7 Sep 2024 09:35:03 +0200 Subject: [PATCH 2/3] Map: fix setStyle() `diff` was always false (#519) --- src/components/Map/BrowserMap.tsx | 15 +++++++++------ src/components/Map/Map.tsx | 7 +++---- src/components/Map/behaviour/useUpdateStyle.tsx | 9 +++++++-- src/components/utils/MapStateContext.tsx | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/components/Map/BrowserMap.tsx b/src/components/Map/BrowserMap.tsx index 409520d1..e83e656a 100644 --- a/src/components/Map/BrowserMap.tsx +++ b/src/components/Map/BrowserMap.tsx @@ -39,16 +39,17 @@ const NotSupportedMessage = () => ( // TODO #460 https://cdn.klokantech.com/openmaptiles-language/v1.0/openmaptiles-language.js + use localized name in FeaturePanel -const BrowserMap = ({ onMapLoaded }) => { +const BrowserMap = () => { const { userLayers } = useMapStateContext(); const mobileMode = useMobileMode(); const { setFeature } = useFeatureContext(); + const { mapLoaded, setMapLoaded } = useMapStateContext(); const [map, mapRef] = useInitMap(); useAddTopRightControls(map, mobileMode); useOnMapClicked(map, setFeature); useOnMapLongPressed(map, setFeature); - useOnMapLoaded(map, onMapLoaded); + useOnMapLoaded(map, setMapLoaded); useFeatureMarker(map); const { viewForMap, setViewFromMap, setBbox, activeLayers } = @@ -56,18 +57,20 @@ const BrowserMap = ({ onMapLoaded }) => { useUpdateViewOnMove(map, setViewFromMap, setBbox); useToggleTerrainControl(map); useUpdateMap(map, viewForMap); - useUpdateStyle(map, activeLayers, userLayers); + useUpdateStyle(map, activeLayers, userLayers, mapLoaded); return
; }; -const BrowserMapCheck = ({ onMapLoaded }) => { +const BrowserMapCheck = () => { + const { setMapLoaded } = useMapStateContext(); + if (!webglSupported) { - onMapLoaded(); + setMapLoaded(); return ; } - return ; + return ; }; export default BrowserMapCheck; // dynamic import diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index cc8afbb9..a8bf9b7f 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -10,7 +10,7 @@ import { SHOW_PROTOTYPE_UI } from '../../config.mjs'; import { LayerSwitcherButton } from '../LayerSwitcher/LayerSwitcherButton'; import { MaptilerLogo } from './MapFooter/MaptilerLogo'; import { TopMenu } from './TopMenu/TopMenu'; -import { webglSupported } from './helpers'; +import { useMapStateContext } from '../utils/MapStateContext'; const BrowserMapDynamic = dynamic(() => import('./BrowserMap'), { ssr: false, @@ -70,12 +70,11 @@ const NoscriptMessage = () => ( ); const Map = () => { - const [mapLoaded, setLoaded, setNotLoaded] = useBoolState(true); - useEffect(setNotLoaded, [setNotLoaded]); + const { mapLoaded } = useMapStateContext(); return ( <> - + {!mapLoaded && } diff --git a/src/components/Map/behaviour/useUpdateStyle.tsx b/src/components/Map/behaviour/useUpdateStyle.tsx index 26d6e620..b97ed46c 100644 --- a/src/components/Map/behaviour/useUpdateStyle.tsx +++ b/src/components/Map/behaviour/useUpdateStyle.tsx @@ -75,7 +75,12 @@ const addOverlaysToStyle = ( }; export const useUpdateStyle = createMapEffectHook( - (map: Map, activeLayers: string[], userLayers: Layer[]) => { + ( + map: Map, + activeLayers: string[], + userLayers: Layer[], + mapLoaded: boolean, + ) => { const [basemap, ...overlays] = activeLayers; const key = basemap ?? DEFAULT_MAP; @@ -86,7 +91,7 @@ export const useUpdateStyle = createMapEffectHook( const style = cloneDeep(getBaseStyle(key)); addOverlaysToStyle(map, style, overlays); - map.setStyle(style, { diff: map.loaded() }); + map.setStyle(style, { diff: mapLoaded }); setUpHover(map, layersWithOsmId(style)); }, diff --git a/src/components/utils/MapStateContext.tsx b/src/components/utils/MapStateContext.tsx index 51d78a80..253a2486 100644 --- a/src/components/utils/MapStateContext.tsx +++ b/src/components/utils/MapStateContext.tsx @@ -1,7 +1,14 @@ -import React, { createContext, useCallback, useContext, useState } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; import { usePersistedState } from './usePersistedState'; import { DEFAULT_MAP } from '../../config.mjs'; import { PROJECT_ID } from '../../services/project'; +import { useBoolState } from '../helpers'; export interface Layer { type: 'basemap' | 'overlay' | 'user' | 'spacer' | 'overlayClimbing'; @@ -31,6 +38,8 @@ type MapStateContextType = { setActiveLayers: (layers: string[] | ((prev: string[]) => string[])) => void; userLayers: Layer[]; setUserLayers: (param: Layer[] | ((current: Layer[]) => Layer[])) => void; + mapLoaded: boolean; + setMapLoaded: () => void; }; export const MapStateContext = createContext(undefined); @@ -51,6 +60,9 @@ export const MapStateProvider = ({ children, initialMapView }) => { [], ); + const [mapLoaded, setMapLoaded, setNotLoaded] = useBoolState(true); + useEffect(setNotLoaded, [setNotLoaded]); + const setBothViews = useCallback((newView) => { setView(newView); setViewForMap(newView); @@ -67,6 +79,8 @@ export const MapStateProvider = ({ children, initialMapView }) => { setActiveLayers, userLayers, setUserLayers, + mapLoaded, + setMapLoaded, }; return ( From c150fffe0f2ab8a38decb8c95c80ac5735903031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Sat, 7 Sep 2024 10:02:34 +0200 Subject: [PATCH 3/3] MemberFeatures: Refactor (#494) --- src/components/App/App.tsx | 10 +- .../Climbing/ClimbingCragDialog.tsx | 49 +++++- .../Climbing/ClimbingCragDialogHeader.tsx | 2 +- .../FeaturePanel/Climbing/ClimbingView.tsx | 4 +- .../Climbing/Editor/GalleryControls.tsx | 2 +- .../FeaturePanel/Climbing/Guide.tsx | 19 +- .../FeaturePanel/Climbing/PanelLabel.tsx | 3 - .../Climbing/RouteDistribution.tsx | 78 ++++----- .../Climbing/RouteList/ExpandedRow.tsx | 20 ++- .../Climbing/RouteList/RouteListRow.tsx | 19 +- .../FeaturePanel/Climbing/RouteNumber.tsx | 72 ++++---- .../Climbing/contexts/ClimbingContext.tsx | 37 +--- .../FeaturePanel/Climbing/utils/photo.ts | 9 + src/components/FeaturePanel/FeaturePanel.tsx | 14 +- .../FeaturePanel/ImagePane/Image/helpers.tsx | 2 +- .../FeaturePanel/MemberFeatures.tsx | 73 -------- .../MemberFeatures/ClimbingItem.tsx | 165 ++++++++++++++++++ .../FeaturePanel/MemberFeatures/Item.tsx | 47 +++++ .../MemberFeatures/MemberFeatures.tsx | 68 ++++++++ src/helpers/theme.tsx | 2 + src/locales/vocabulary.js | 4 + 21 files changed, 463 insertions(+), 236 deletions(-) delete mode 100644 src/components/FeaturePanel/MemberFeatures.tsx create mode 100644 src/components/FeaturePanel/MemberFeatures/ClimbingItem.tsx create mode 100644 src/components/FeaturePanel/MemberFeatures/Item.tsx create mode 100644 src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index db1975db..038c7273 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -87,7 +87,10 @@ const IndexWithProviders = () => { // TODO add correct error boundaries const isClimbingDialogShown = router.query.all?.[2] === 'climbing'; - const photo = router.query.all?.[3]; + const photo = + router.query.all?.[3] === 'photo' ? router.query.all?.[4] : undefined; + const routeNumber = + router.query.all?.[3] === 'route' ? router.query.all?.[4] : undefined; return ( <> @@ -97,7 +100,10 @@ const IndexWithProviders = () => { {featureShown && isMobileMode && } {isClimbingDialogShown && ( - + )} diff --git a/src/components/FeaturePanel/Climbing/ClimbingCragDialog.tsx b/src/components/FeaturePanel/Climbing/ClimbingCragDialog.tsx index 7cf75ff4..8ea5afc9 100644 --- a/src/components/FeaturePanel/Climbing/ClimbingCragDialog.tsx +++ b/src/components/FeaturePanel/Climbing/ClimbingCragDialog.tsx @@ -1,7 +1,7 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import styled from '@emotion/styled'; import AddIcon from '@mui/icons-material/Add'; -import Router from 'next/router'; +import Router, { useRouter } from 'next/router'; import { Dialog, DialogContent, DialogActions, Button } from '@mui/material'; import { ClimbingView } from './ClimbingView'; import { useClimbingContext } from './contexts/ClimbingContext'; @@ -9,6 +9,7 @@ import { ClimbingCragDialogHeader } from './ClimbingCragDialogHeader'; import { getOsmappLink } from '../../../services/helpers'; import { useFeatureContext } from '../../utils/FeatureContext'; import { useGetHandleSave } from './useGetHandleSave'; +import { getWikimediaCommonsPhotoKeys, removeFilePrefix } from './utils/photo'; const Flex = styled.div` display: flex; @@ -16,7 +17,13 @@ const Flex = styled.div` width: 100%; `; -export const ClimbingCragDialog = ({ photo }: { photo?: string }) => { +export const ClimbingCragDialog = ({ + photo, + routeNumber, +}: { + photo?: string; + routeNumber?: number; +}) => { const contentRef = useRef(null); const { @@ -26,11 +33,45 @@ export const ClimbingCragDialog = ({ photo }: { photo?: string }) => { isEditMode, getMachine, showDebugMenu, + setRouteSelectedIndex, + routes, + setPhotoPath, + photoPath, + photoPaths, } = useClimbingContext(); const { feature } = useFeatureContext(); const handleSave = useGetHandleSave(setIsEditMode); - // const { routes } = useClimbingContext(); const machine = getMachine(); + const router = useRouter(); + const featureLink = getOsmappLink(feature); + + useEffect(() => { + const tags = routes[routeNumber]?.feature?.tags || {}; + const photos = getWikimediaCommonsPhotoKeys(tags); + + if (routeNumber !== undefined && photos?.[0]) { + setRouteSelectedIndex(routeNumber); + const firstPhoto = tags[photos[0]]; + const newPhotoPath = removeFilePrefix(firstPhoto); + router.replace(`${featureLink}/climbing/photo/${newPhotoPath}`); + setPhotoPath(newPhotoPath); + } else if (photo) { + setPhotoPath(photo); + } else if (!photoPath && photoPaths?.length > 0) { + setPhotoPath(photoPaths[0]); + if (routeNumber !== undefined) setRouteSelectedIndex(routeNumber); + } + }, [ + featureLink, + photo, + photoPath, + photoPaths, + routeNumber, + router, + routes, + setPhotoPath, + setRouteSelectedIndex, + ]); const onScroll = (e) => { setScrollOffset({ diff --git a/src/components/FeaturePanel/Climbing/ClimbingCragDialogHeader.tsx b/src/components/FeaturePanel/Climbing/ClimbingCragDialogHeader.tsx index 7f4846c4..4b4b2741 100644 --- a/src/components/FeaturePanel/Climbing/ClimbingCragDialogHeader.tsx +++ b/src/components/FeaturePanel/Climbing/ClimbingCragDialogHeader.tsx @@ -54,7 +54,7 @@ export const ClimbingCragDialogHeader = ({ onClose }) => { const onPhotoChange = (photo: string) => { Router.push( - `${getOsmappLink(feature)}/climbing/${photo}${window.location.hash}`, + `${getOsmappLink(feature)}/climbing/photo/${photo}${window.location.hash}`, ); setAreRoutesLoading(true); diff --git a/src/components/FeaturePanel/Climbing/ClimbingView.tsx b/src/components/FeaturePanel/Climbing/ClimbingView.tsx index c80c7957..bd0c80c4 100644 --- a/src/components/FeaturePanel/Climbing/ClimbingView.tsx +++ b/src/components/FeaturePanel/Climbing/ClimbingView.tsx @@ -192,7 +192,7 @@ export const ClimbingView = ({ photo }: { photo?: string }) => { photoPath, loadPhotoRelatedData, areRoutesLoading, - preparePhotosAndSet, + preparePhotos, photoZoom, loadedPhotos, } = useClimbingContext(); @@ -252,7 +252,7 @@ export const ClimbingView = ({ photo }: { photo?: string }) => { const cragPhotos = getWikimediaCommonsKeys(feature.tags) .map((key) => feature.tags[key]) .map(removeFilePrefix); - preparePhotosAndSet(cragPhotos, photo); + preparePhotos(cragPhotos); useEffect(() => { const handleResize = () => { diff --git a/src/components/FeaturePanel/Climbing/Editor/GalleryControls.tsx b/src/components/FeaturePanel/Climbing/Editor/GalleryControls.tsx index f2a8322d..d02c029f 100644 --- a/src/components/FeaturePanel/Climbing/Editor/GalleryControls.tsx +++ b/src/components/FeaturePanel/Climbing/Editor/GalleryControls.tsx @@ -61,7 +61,7 @@ export const GalleryControls = () => { photoIndex === photoPaths.length - 1 ? null : photoIndex + 1; const onPhotoChange = (photo: string) => { Router.push( - `${getOsmappLink(feature)}/climbing/${photo}${window.location.hash}`, + `${getOsmappLink(feature)}/climbing/photo/${photo}${window.location.hash}`, ); setAreRoutesLoading(true); diff --git a/src/components/FeaturePanel/Climbing/Guide.tsx b/src/components/FeaturePanel/Climbing/Guide.tsx index 82dd60f1..e85cf75c 100644 --- a/src/components/FeaturePanel/Climbing/Guide.tsx +++ b/src/components/FeaturePanel/Climbing/Guide.tsx @@ -6,6 +6,9 @@ import { t } from '../../../services/intl'; import { useClimbingContext } from './contexts/ClimbingContext'; import { RouteNumber } from './RouteNumber'; import { useFeatureContext } from '../../utils/FeatureContext'; +import { isTicked } from '../../../services/ticks'; +import { getWikimediaCommonsPhotoKeys } from './utils/photo'; +import { getShortId } from '../../../services/helpers'; const DrawRouteButton = styled(Button)` align-items: baseline; @@ -17,6 +20,7 @@ export const Guide = () => { useClimbingContext(); const machine = getMachine(); const path = getCurrentPath(); + const { feature } = useFeatureContext(); const handleClose = () => { setIsGuideClosed(true); @@ -29,10 +33,11 @@ export const Guide = () => { !isInSchema && machine.currentStateName !== 'extendRoute'; const { - feature: { - osmMeta: { id }, - }, + feature: { osmMeta }, } = useFeatureContext(); + const photosCount = getWikimediaCommonsPhotoKeys(feature.tags).length; + const hasTick = isTicked(getShortId(osmMeta)); + return ( { size="small" onClick={onDrawRouteClick} > - Zakreslit cestu   + {t('climbingpanel.draw_route')}   0} + hasTick={hasTick} + hasTooltip={false} > {routeSelectedIndex + 1} diff --git a/src/components/FeaturePanel/Climbing/PanelLabel.tsx b/src/components/FeaturePanel/Climbing/PanelLabel.tsx index 63f81c1a..0c99e5d8 100644 --- a/src/components/FeaturePanel/Climbing/PanelLabel.tsx +++ b/src/components/FeaturePanel/Climbing/PanelLabel.tsx @@ -9,9 +9,6 @@ type PanelLabelProps = { }; export const Container = styled.div<{ $border: boolean }>` - ${({ $border, theme }) => - $border ? `border-bottom: solid 1px ${theme.palette.divider};` : ''} - padding: 20px 10px 4px; `; diff --git a/src/components/FeaturePanel/Climbing/RouteDistribution.tsx b/src/components/FeaturePanel/Climbing/RouteDistribution.tsx index 955db092..77cc1000 100644 --- a/src/components/FeaturePanel/Climbing/RouteDistribution.tsx +++ b/src/components/FeaturePanel/Climbing/RouteDistribution.tsx @@ -98,52 +98,38 @@ export const RouteDistribution = () => { })); return ( - <> - { - setUserSetting('climbing.gradeSystem', system); - }} - selectedGradeSystem={userSettings['climbing.gradeSystem']} - /> - } - > - Routes distribution - - - - - {heightsRatios.map((heightRatioItem) => { - const color = getDifficultyColor( - { - gradeSystem: 'uiaa', - grade: heightRatioItem.grade, - }, - theme, - ); - const numberOfRoutesKey = Object.keys(routeOccurrences).find( - (key) => key === heightRatioItem.grade, - ); - const numberOfRoutes = routeOccurrences[numberOfRoutesKey]; - const isColumnActive = numberOfRoutes > 0; - return ( - - {numberOfRoutes > 0 && ( - {numberOfRoutes}x - )} - - - - {heightRatioItem.grade} - - - ); - })} - - - - + + + + {heightsRatios.map((heightRatioItem) => { + const color = getDifficultyColor( + { + gradeSystem: 'uiaa', + grade: heightRatioItem.grade, + }, + theme, + ); + const numberOfRoutesKey = Object.keys(routeOccurrences).find( + (key) => key === heightRatioItem.grade, + ); + const numberOfRoutes = routeOccurrences[numberOfRoutesKey]; + const isColumnActive = numberOfRoutes > 0; + return ( + + {numberOfRoutes > 0 && ( + {numberOfRoutes}x + )} + + + + {heightRatioItem.grade} + + + ); + })} + + + ); }; diff --git a/src/components/FeaturePanel/Climbing/RouteList/ExpandedRow.tsx b/src/components/FeaturePanel/Climbing/RouteList/ExpandedRow.tsx index 9520265f..fdde41d6 100644 --- a/src/components/FeaturePanel/Climbing/RouteList/ExpandedRow.tsx +++ b/src/components/FeaturePanel/Climbing/RouteList/ExpandedRow.tsx @@ -20,7 +20,7 @@ import { RouteInDifferentPhotos } from './RouteInDifferentPhotos'; import { Label } from './Label'; import { getOsmappLink } from '../../../../services/helpers'; import { MyRouteTicks } from '../Ticks/MyRouteTicks'; - +import Link from 'next/link'; const Left = styled.div` flex: 1; `; @@ -170,18 +170,20 @@ export const ExpandedRow = ({ {tempRoute.feature ? ( - + + ) : null} diff --git a/src/components/FeaturePanel/Climbing/RouteList/RouteListRow.tsx b/src/components/FeaturePanel/Climbing/RouteList/RouteListRow.tsx index 7f5c06d8..ff06d45b 100644 --- a/src/components/FeaturePanel/Climbing/RouteList/RouteListRow.tsx +++ b/src/components/FeaturePanel/Climbing/RouteList/RouteListRow.tsx @@ -14,6 +14,8 @@ import { ConvertedRouteDifficultyBadge } from '../ConvertedRouteDifficultyBadge' import { getShortId } from '../../../../services/helpers'; import { getDifficulties } from '../utils/grades/routeGrade'; import { TickedRouteCheck } from '../Ticks/TickedRouteCheck'; +import { isTicked } from '../../../../services/ticks'; +import { getWikimediaCommonsPhotoKeys } from '../utils/photo'; const DEBOUNCE_TIME = 1000; const Container = styled.div` @@ -37,6 +39,7 @@ const DifficultyCell = styled(Cell)` const RouteNumberCell = styled(Cell)` color: #999; margin-left: 8px; + margin-right: 8px; `; const ExpandIcon = styled(ExpandMoreIcon)<{ $isExpanded: boolean }>` transform: rotate(${({ $isExpanded }) => ($isExpanded ? 180 : 0)}deg); @@ -76,12 +79,10 @@ export const RenderListRow = ({ const { getMachine, - isRouteSelected, isEditMode, routeSelectedIndex, routeIndexExpanded, setRouteIndexExpanded, - getPhotoInfoForRoute, } = useClimbingContext(); useEffect(() => { @@ -92,8 +93,6 @@ export const RenderListRow = ({ const osmId = route.feature?.osmMeta ? getShortId(route.feature.osmMeta) : null; - const isSelected = isRouteSelected(index); - const photoInfoForRoute = getPhotoInfoForRoute(index); const machine = getMachine(); @@ -128,16 +127,16 @@ export const RenderListRow = ({ osmId, }; const routeDifficulties = getDifficulties(tempRoute.feature?.tags); + const hasTick = isTicked(osmId); + const photosCount = getWikimediaCommonsPhotoKeys( + tempRoute.feature?.tags || {}, + ).length; return ( - - + + 0} hasTick={hasTick}> {index + 1} diff --git a/src/components/FeaturePanel/Climbing/RouteNumber.tsx b/src/components/FeaturePanel/Climbing/RouteNumber.tsx index 87c86849..113417a3 100644 --- a/src/components/FeaturePanel/Climbing/RouteNumber.tsx +++ b/src/components/FeaturePanel/Climbing/RouteNumber.tsx @@ -4,65 +4,61 @@ import styled from '@emotion/styled'; import { Tooltip } from '@mui/material'; import { useRouteNumberColors } from './utils/useRouteNumberColors'; import { isTicked } from '../../../services/ticks'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -const Container = styled.div<{ - $colors: Record; +const Container = styled.div` + position: relative; +`; +const TickCheckContainer = styled.div` + position: absolute; + top: -5px; + right: -5px; + font-size: 12px; +`; + +const Circle = styled.div<{ + $hasCircle: boolean; }>` width: 20px; height: 20px; line-height: 20px; border-radius: 50%; - background: ${({ $colors }) => $colors.background}; - color: ${({ $colors }) => $colors.text}; + background: ${({ theme, $hasCircle }) => + $hasCircle ? theme.palette.climbing.primary : undefined}; + color: ${({ theme, $hasCircle }) => + $hasCircle ? theme.palette.climbing.secondary : '#999'}; display: flex; justify-content: center; align-items: center; font-size: 12px; - font-weight: 600; - - border: ${({ $colors }) => $colors.border}; `; export const RouteNumber = ({ children, - isSelected, - photoInfoForRoute, - osmId, + hasCircle = false, + hasTick = false, + hasTooltip = true, }) => { - const hasPathOnThisPhoto = photoInfoForRoute === 'hasPathOnThisPhoto'; - const isOnThisPhoto = photoInfoForRoute === 'isOnThisPhoto'; - const hasPathInDifferentPhoto = - photoInfoForRoute === 'hasPathInDifferentPhoto'; - const isOnDifferentPhoto = photoInfoForRoute === 'isOnDifferentPhoto'; - - const colors = useRouteNumberColors({ - isSelected, - hasPathOnThisPhoto, - isOnThisPhoto, - hasPathInDifferentPhoto, - isOnDifferentPhoto, - isTicked: isTicked(osmId), - }); - const getTitle = () => { - if (hasPathOnThisPhoto) { - return 'Route has path on this photo'; - } - if (isOnThisPhoto) { - return 'Route is on this photo'; - } - if (hasPathInDifferentPhoto) { - return 'Route has path available on different photo'; + if (hasTick) { + return 'You ticked this route'; } - if (isOnDifferentPhoto) { - return 'Route is available on different photo'; + if (hasCircle) { + return 'Route has marked path'; } - return 'Route is not marked yet'; + return 'Route has no marked path'; }; return ( - - {children} + + + {children} + {hasTick && ( + + + + )} + ); }; diff --git a/src/components/FeaturePanel/Climbing/contexts/ClimbingContext.tsx b/src/components/FeaturePanel/Climbing/contexts/ClimbingContext.tsx index ac1e0713..c1670e17 100644 --- a/src/components/FeaturePanel/Climbing/contexts/ClimbingContext.tsx +++ b/src/components/FeaturePanel/Climbing/contexts/ClimbingContext.tsx @@ -48,7 +48,6 @@ type ClimbingContextType = { isRouteSelected: (routeNumber: number) => boolean; isRouteHovered: (routeNumber: number) => boolean; isPointSelected: (pointNumber: number) => boolean; - getPhotoInfoForRoute: (routeNumber: number) => PhotoInfo; pointSelectedIndex: number; routes: Array; routeSelectedIndex: number; @@ -115,7 +114,7 @@ type ClimbingContextType = { setShowDebugMenu: (showDebugMenu: boolean) => void; arePointerEventsDisabled: boolean; // @TODO do we need it? setArePointerEventsDisabled: (arePointerEventsDisabled: boolean) => void; - preparePhotosAndSet: (cragPhotos: Array, photo?: string) => void; + preparePhotos: (cragPhotos: Array) => void; }; // @TODO generate? @@ -259,33 +258,6 @@ export const ClimbingContextProvider = ({ children, feature }: Props) => { const isRouteSelected = (index: number) => routeSelectedIndex === index; const isRouteHovered = (index: number) => routeIndexHovered === index; const isPointSelected = (index: number) => pointSelectedIndex === index; - const getPhotoInfoForRoute = (index: number): PhotoInfo => { - const checkedPaths = routes[index]?.paths; - if (!checkedPaths) return null; - const availablePhotos = Object.keys(checkedPaths); - - return availablePhotos.reduce( - (photoInfo, availablePhotoPath) => { - if ( - !checkedPaths[availablePhotoPath] || - photoInfo === 'hasPathOnThisPhoto' || - photoInfo === 'isOnThisPhoto' - ) - return photoInfo; - - if (availablePhotoPath === photoPath) { - if (checkedPaths[availablePhotoPath].length > 0) - return 'hasPathOnThisPhoto'; - return 'isOnThisPhoto'; - } - - if (checkedPaths[availablePhotoPath].length > 0) - return 'hasPathInDifferentPhoto'; - return 'isOnDifferentPhoto'; - }, - null, - ); - }; const getAllRoutesPhotos = (cragPhotos: Array) => { const photos = routes.reduce((acc, route) => { @@ -297,10 +269,8 @@ export const ClimbingContextProvider = ({ children, feature }: Props) => { setPhotoPaths(photos); }; - const preparePhotosAndSet = (cragPhotos: Array, photo?: string) => { + const preparePhotos = (cragPhotos: Array) => { if (photoPaths === null) getAllRoutesPhotos(cragPhotos); - if (!photoPath && photoPaths?.length > 0) - setPhotoPath(photo || photoPaths[0]); }; const loadPhotoRelatedData = () => { @@ -335,7 +305,6 @@ export const ClimbingContextProvider = ({ children, feature }: Props) => { isRouteSelected, isRouteHovered, isPointSelected, - getPhotoInfoForRoute, pointSelectedIndex, routes, routeSelectedIndex, @@ -386,7 +355,7 @@ export const ClimbingContextProvider = ({ children, feature }: Props) => { setShowDebugMenu, arePointerEventsDisabled, setArePointerEventsDisabled, - preparePhotosAndSet, + preparePhotos, imageContainerSize, setImageContainerSize, loadedPhotos, diff --git a/src/components/FeaturePanel/Climbing/utils/photo.ts b/src/components/FeaturePanel/Climbing/utils/photo.ts index c7e2ae98..39b30bcd 100644 --- a/src/components/FeaturePanel/Climbing/utils/photo.ts +++ b/src/components/FeaturePanel/Climbing/utils/photo.ts @@ -11,6 +11,15 @@ export const removeFilePrefix = (name: string) => name?.replace(/^File:/, ''); export const isWikimediaCommons = (tag: string) => tag.startsWith('wikimedia_commons'); +export const isWikimediaCommonsPhoto = (tag: string) => { + // regexp to match wikimedia_commons, wikimedia_commons:2, etc. but not wikimedia_commons:path, wikimedia_commons:whatever + const re = /^wikimedia_commons(:\d+)?$/; + return re.test(tag); +}; + +export const getWikimediaCommonsPhotoKeys = (tags: FeatureTags) => + Object.keys(tags).filter(isWikimediaCommonsPhoto); + export const getWikimediaCommonsKeys = (tags: FeatureTags) => Object.keys(tags).filter(isWikimediaCommons); // TODO this returns also :path keys, not sure if intended diff --git a/src/components/FeaturePanel/FeaturePanel.tsx b/src/components/FeaturePanel/FeaturePanel.tsx index 677a0806..59292ebd 100644 --- a/src/components/FeaturePanel/FeaturePanel.tsx +++ b/src/components/FeaturePanel/FeaturePanel.tsx @@ -9,7 +9,7 @@ import { OsmError } from './OsmError'; import { Members } from './Members'; import { PublicTransport } from './PublicTransport/PublicTransport'; import { Properties } from './Properties/Properties'; -import { MemberFeatures } from './MemberFeatures'; +import { MemberFeatures } from './MemberFeatures/MemberFeatures'; import { ParentLink } from './ParentLink'; import { FeatureImages } from './ImagePane/FeatureImages'; import { FeatureOpenPlaceGuideLink } from './FeatureOpenPlaceGuideLink'; @@ -43,6 +43,11 @@ export const FeaturePanel = () => { // Different components are shown for different types of features // Conditional components should have if(feature.tags.xxx) check at the beggining // All components should have margin-bottoms to accomodate missing parts + const isClimbingCrag = tags.climbing === 'crag'; + + const PropertiesComponent = () => ( + + ); return ( <> @@ -65,14 +70,13 @@ export const FeaturePanel = () => { - - - - + {!isClimbingCrag && } + {advanced && } + {isClimbingCrag && } diff --git a/src/components/FeaturePanel/ImagePane/Image/helpers.tsx b/src/components/FeaturePanel/ImagePane/Image/helpers.tsx index 37511c0c..2b6106df 100644 --- a/src/components/FeaturePanel/ImagePane/Image/helpers.tsx +++ b/src/components/FeaturePanel/ImagePane/Image/helpers.tsx @@ -45,7 +45,7 @@ export const useGetOnClick = (def: ImageDef) => { return () => { const featureLink = getOsmappLink(feature); const photoLink = removeFilePrefix(def.v); - Router.push(`${featureLink}/climbing/${photoLink}`); + Router.push(`${featureLink}/climbing/photo/${photoLink}`); }; } diff --git a/src/components/FeaturePanel/MemberFeatures.tsx b/src/components/FeaturePanel/MemberFeatures.tsx deleted file mode 100644 index b2936ed2..00000000 --- a/src/components/FeaturePanel/MemberFeatures.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import Router from 'next/router'; -import { getOsmappLink, getUrlOsmId } from '../../services/helpers'; -import { useFeatureContext } from '../utils/FeatureContext'; -import { Feature } from '../../services/types'; -import { getLabel } from '../../helpers/featureLabel'; -import { useUserThemeContext } from '../../helpers/theme'; -import { useMobileMode } from '../helpers'; -import Maki from '../utils/Maki'; -import { PanelLabel } from './Climbing/PanelLabel'; - -const Item = ({ feature }: { feature: Feature }) => { - const { currentTheme } = useUserThemeContext(); - const mobileMode = useMobileMode(); - const { setPreview } = useFeatureContext(); - const { properties, tags, osmMeta } = feature; - const handleClick = (e) => { - e.preventDefault(); - setPreview(null); - Router.push(`/${getUrlOsmId(osmMeta)}${window.location.hash}`); - }; - const handleHover = () => feature.center && setPreview(feature); - - return ( -
  • - setPreview(null)} - > - - {getLabel(feature)} - -
  • - ); -}; - -export const MemberFeatures = () => { - const { - feature: { memberFeatures, tags }, - } = useFeatureContext(); - - if (!memberFeatures?.length) { - return null; - } - - const isClimbingArea = tags.climbing === 'area'; - if (isClimbingArea) { - return null; - } - - const isClimbingCrag = tags.climbing === 'crag'; - const heading = isClimbingCrag ? 'Routes' : 'Subitems'; - - return ( - - {heading} -
      - {memberFeatures.map((item) => ( - - ))} -
    -
    - ); -}; diff --git a/src/components/FeaturePanel/MemberFeatures/ClimbingItem.tsx b/src/components/FeaturePanel/MemberFeatures/ClimbingItem.tsx new file mode 100644 index 00000000..4a5f1024 --- /dev/null +++ b/src/components/FeaturePanel/MemberFeatures/ClimbingItem.tsx @@ -0,0 +1,165 @@ +import { Feature } from '../../../services/types'; +import React from 'react'; +import styled from '@emotion/styled'; +import { ConvertedRouteDifficultyBadge } from '../Climbing/ConvertedRouteDifficultyBadge'; +import { getDifficulties } from '../Climbing/utils/grades/routeGrade'; +import CheckIcon from '@mui/icons-material/Check'; +import { getWikimediaCommonsPhotoKeys } from '../Climbing/utils/photo'; +import { RouteNumber } from '../Climbing/RouteNumber'; +import { isTicked, onTickAdd } from '../../../services/ticks'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import { getOsmappLink, getShortId } from '../../../services/helpers'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import { t } from '../../../services/intl'; +import { IconButton, Menu, MenuItem } from '@mui/material'; +import Router from 'next/router'; +import { useMobileMode } from '../../helpers'; +import { useSnackbar } from '../../utils/SnackbarContext'; +import { useUserSettingsContext } from '../../utils/UserSettingsContext'; +import Link from 'next/link'; + +const RoutePhoto = styled.div` + width: 20px; +`; + +const RouteName = styled.div` + flex: 1; +`; + +const RouteGrade = styled.div``; + +const Container = styled.a` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex: 1; + gap: 12px; + border-bottom: solid 1px ${({ theme }) => theme.palette.divider}; + color: ${({ theme }) => theme.palette.text.primary}; + cursor: pointer; + padding: 8px 20px; + margin: 0px -12px; + transition: all 0.1s; + *, + &:focus { + text-decoration: none; + } + + &:hover { + text-decoration: none; + background-color: rgba(0, 0, 0, 0.1); + } +`; + +const useMoreMenu = () => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClickMore = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + event.preventDefault(); + event.stopPropagation(); + }; + + const handleCloseMore = (event) => { + setAnchorEl(null); + event.stopPropagation(); + }; + + const MoreMenu = ({ children }) => ( + + {children} + + ); + + return { anchorEl, open, handleClickMore, handleCloseMore, MoreMenu }; +}; + +export const ClimbingItem = ({ + feature, + index, + cragFeature, +}: { + feature: Feature; + index: number; + cragFeature: Feature; +}) => { + const routeNumber = index + 1; + const { showToast } = useSnackbar(); + const { userSettings } = useUserSettingsContext(); + const mobileMode = useMobileMode(); + const { setPreview } = useFeatureContext(); + const { MoreMenu, handleClickMore, handleCloseMore } = useMoreMenu(); + + if (!feature) return null; + const shortOsmId = getShortId(feature.osmMeta); + const routeDifficulties = getDifficulties(feature.tags); + const photosCount = getWikimediaCommonsPhotoKeys(feature.tags).length; + const shortId = getShortId(feature.osmMeta); + const hasTick = isTicked(shortId); + + const routeDetailUrl = `${getOsmappLink(feature)}${typeof window !== 'undefined' ? window.location.hash : ''}`; + + const handleClickItem = (event) => { + event.preventDefault(); + event.stopPropagation(); + const cragFeatureLink = getOsmappLink(cragFeature); + Router.push(`${cragFeatureLink}/climbing/route/${index}`); + }; + + const handleHover = () => feature.center && setPreview(feature); + + const handleShowRouteDetail = (event) => { + handleCloseMore(event); + event.stopPropagation(); + }; + + const handleAddTick = (event) => { + onTickAdd({ + osmId: shortOsmId, + style: userSettings['climbing.defaultClimbingStyle'], + }); + showToast('Tick added!', 'success'); + handleCloseMore(event); + event.stopPropagation(); + }; + + return ( + setPreview(null)} + > + + 0} hasTick={hasTick}> + {routeNumber} + + + {feature.tags?.name} + + + + + + + + + + + + {t('climbingpanel.add_tick')} + + + + {t('climbingpanel.show_route_detail')} + + + + ); +}; diff --git a/src/components/FeaturePanel/MemberFeatures/Item.tsx b/src/components/FeaturePanel/MemberFeatures/Item.tsx new file mode 100644 index 00000000..8a9ea291 --- /dev/null +++ b/src/components/FeaturePanel/MemberFeatures/Item.tsx @@ -0,0 +1,47 @@ +import { Feature } from '../../../services/types'; +import { useUserThemeContext } from '../../../helpers/theme'; +import { useMobileMode } from '../../helpers'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import Router from 'next/router'; +import { getUrlOsmId } from '../../../services/helpers'; +import Maki from '../../utils/Maki'; +import { getLabel } from '../../../helpers/featureLabel'; +import React from 'react'; +import styled from '@emotion/styled'; + +const Li = styled.li` + margin-left: 10px; +`; + +export const Item = ({ feature }: { feature: Feature }) => { + const { currentTheme } = useUserThemeContext(); + const mobileMode = useMobileMode(); + const { setPreview } = useFeatureContext(); + const { properties, tags, osmMeta } = feature; + const handleClick = (e) => { + e.preventDefault(); + setPreview(null); + Router.push(`/${getUrlOsmId(osmMeta)}${window.location.hash}`); + }; + const handleHover = () => feature.center && setPreview(feature); + + return ( +
  • + setPreview(null)} + > + + {getLabel(feature)} + +
  • + ); +}; diff --git a/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx b/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx new file mode 100644 index 00000000..cdc1b343 --- /dev/null +++ b/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Box } from '@mui/material'; +import { getOsmappLink, getShortId } from '../../../services/helpers'; +import { useFeatureContext } from '../../utils/FeatureContext'; +import { PanelLabel } from '../Climbing/PanelLabel'; +import { Item } from './Item'; +import { ClimbingItem } from './ClimbingItem'; +import styled from '@emotion/styled'; +import { GradeSystemSelect } from '../Climbing/GradeSystemSelect'; +import { useUserSettingsContext } from '../../utils/UserSettingsContext'; +import { RouteDistributionInPanel } from '../Climbing/RouteDistribution'; + +const Ul = styled.ul` + padding: 0; + list-style: none; +`; + +export const MemberFeatures = () => { + const { feature } = useFeatureContext(); + const { memberFeatures, tags } = feature; + + const { userSettings, setUserSetting } = useUserSettingsContext(); + + if (!memberFeatures?.length) { + return null; + } + + const isClimbingArea = tags.climbing === 'area'; + if (isClimbingArea) { + return null; + } + + const isClimbingCrag = tags.climbing === 'crag'; + const heading = isClimbingCrag ? 'Climbing routes' : 'Subitems'; + + const ItemComponent = isClimbingCrag ? ClimbingItem : Item; + + return ( + + { + setUserSetting('climbing.gradeSystem', system); + }} + selectedGradeSystem={userSettings['climbing.gradeSystem']} + /> + } + > + {heading} ({memberFeatures.length}) + +
      + {memberFeatures.map((item, index) => + isClimbingCrag ? ( + + ) : ( + + ), + )} +
    +
    + ); +}; diff --git a/src/helpers/theme.tsx b/src/helpers/theme.tsx index 5c628a11..5ce0ebaf 100644 --- a/src/helpers/theme.tsx +++ b/src/helpers/theme.tsx @@ -6,6 +6,7 @@ import { useMediaQuery } from '@mui/material'; const lightTheme = createTheme({ palette: { + divider: 'rgba(0, 0, 0, 0.04)', primary: { main: '#556cd6', }, @@ -45,6 +46,7 @@ const lightTheme = createTheme({ const darkTheme = createTheme({ palette: { mode: 'dark', + divider: 'rgba(255, 255, 255, 0.04)', primary: { main: '#ffb74d', }, diff --git a/src/locales/vocabulary.js b/src/locales/vocabulary.js index fd96de93..de9825b8 100644 --- a/src/locales/vocabulary.js +++ b/src/locales/vocabulary.js @@ -225,6 +225,10 @@ export default { 'climbingpanel.delete_climbing_route': 'Delete route __route__ in schema', 'climbingpanel.create_first_node': 'Click on the beginning of the route', 'climbingpanel.create_next_node': 'Follow direction of the route', + 'climbingpanel.draw_route': 'Draw route', + + 'climbingpanel.show_route_detail': 'Show route detail', + 'climbingpanel.add_tick': 'Add tick', 'runway.information': 'Runway information', 'runway.runway': 'Runway',