From 0861c9d7c4f0d0b1ef1d0a0254f59560246a0f9c Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 24 Apr 2026 12:48:42 +0300 Subject: [PATCH 01/16] Update the favorite name input in Add/Edit favorite panel --- .../favorite/structure/FavoriteName.jsx | 49 +++++++------------ .../translations/en/web-translation.json | 2 + 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx index cd21001c94..afc763ed66 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx @@ -27,16 +27,15 @@ export default function FavoriteName({ } let group = ctx.favorites?.mapObjs?.[id]; let names = []; - group && - group.wpts.forEach((wpt) => { - if (favorite) { - if (wpt.name !== favorite.name) { - names.push(wpt.name); - } - } else { + group?.wpts.forEach((wpt) => { + if (favorite) { + if (wpt.name !== favorite.name) { names.push(wpt.name); } - }); + } else { + names.push(wpt.name); + } + }); validateName(favoriteName, names); setFavNames(names); }, [favoriteGroup]); @@ -60,14 +59,14 @@ export default function FavoriteName({ setErrorName(nameExists); } - function gerErrorText(favoriteName) { - if (favoriteName === '') { - return 'Empty name!'; + function getErrorText(name) { + if (name === '') { + return t('web:fav_name_empty'); } else if (nameAlreadyExist) { - return 'This name already exists!'; - } else { - return ' '; + return t('web:fav_name_already_exists'); } + + return ' '; } useEffect(() => { @@ -89,29 +88,15 @@ export default function FavoriteName({ setFavoriteName(e.target.value)} value={favoriteName} autoFocus error={favoriteName === '' || nameAlreadyExist} - helperText={gerErrorText(favoriteName)} - sx={{ - maxWidth: '450px !important', - resize: 'none', - fontFamily: 'Arial', - color: 'black', - fontSize: 20, - ml: '-2px', - borderColor: '#bebdb4', - backgroundColor: 'transparent', - outlineColor: '#757575', - cursor: 'pointer', - '&[disabled]': { border: 'none' }, - mb: '-10px', - pb: '8px', - pt: '8px', - }} + helperText={getErrorText(favoriteName)} /> ); diff --git a/map/src/resources/translations/en/web-translation.json b/map/src/resources/translations/en/web-translation.json index 7be8f228e5..c0fd2205c3 100644 --- a/map/src/resources/translations/en/web-translation.json +++ b/map/src/resources/translations/en/web-translation.json @@ -315,6 +315,8 @@ "rename_track_not_found": "Track not found.", "rename_empty_folder_name": "Empty folder name.", "rename_folder_already_exists": "Folder already exists.", + "fav_name_empty": "Empty name.", + "fav_name_already_exists": "This name already exists.", "rename_item_not_found": "Folder or track was not found.", "garmin_sync_title": "Sync your activities", "garmin_sync_description": "Connect your account to sync Garmin Connect™ activities with OsmAnd.", From 4cbb2355f806c06c9e44a4ff730690c7d6a21602 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 24 Apr 2026 14:31:15 +0300 Subject: [PATCH 02/16] Update address input in Add/Edit favorite panel --- .../components/favorite/WptEditPanel.jsx | 4 +- .../favorite/structure/FavoriteAddress.jsx | 75 +++++++++---------- .../favorite/structure/FavoriteName.jsx | 7 +- .../favorite/wptEditPanel.module.css | 2 +- .../infoblock/components/wpt/WptDetails.jsx | 37 +++++---- .../translations/en/web-translation.json | 2 + 6 files changed, 63 insertions(+), 64 deletions(-) diff --git a/map/src/infoblock/components/favorite/WptEditPanel.jsx b/map/src/infoblock/components/favorite/WptEditPanel.jsx index 7bd91037b6..07ae48cfa5 100644 --- a/map/src/infoblock/components/favorite/WptEditPanel.jsx +++ b/map/src/infoblock/components/favorite/WptEditPanel.jsx @@ -55,7 +55,7 @@ export default function WptEditPanel({ setShowInfoBlock }) { const [favoriteName, setFavoriteName] = useState(editWpt?.name ?? ''); const [favoriteAddress, setFavoriteAddress] = useState(editWpt?.address ?? ctx.addFavorite?.address ?? ''); const [favoriteDescription, setFavoriteDescription] = useState(editWpt?.desc ?? ''); - const [addAddress, setAddAddress] = useState(isEditMode || isPoi); + const [addAddress, setAddAddress] = useState(isEditMode || isPoi || (isAddMode && !isAddTrackWpt)); const [addDescription, setAddDescription] = useState(isEditMode); const [favoriteGroup, setFavoriteGroup] = useState(null); const [favoriteIcon, setFavoriteIcon] = useState( @@ -486,8 +486,8 @@ export default function WptEditPanel({ setShowInfoBlock }) { )} {!addDescription && ( diff --git a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx index a8463f0c27..1ce9aa7d79 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx @@ -1,48 +1,47 @@ -import { IconButton, ListItemText, TextField } from '@mui/material'; -import { Delete } from '@mui/icons-material'; -import React from 'react'; +import { Box, IconButton, InputAdornment, TextField } from '@mui/material'; +import { ReactComponent as CancelIcon } from '../../../../assets/icons/ic_action_cancel.svg'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getAddressByLatLon } from '../../wpt/WptDetails'; + +export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, widthDialog, latLon }) { + const { t } = useTranslation(); + + const [searching, setSearching] = useState(false); + + useEffect(() => { + if (!latLon?.lat || !latLon?.lon || favoriteAddress) return; + setSearching(true); + getAddressByLatLon(latLon.lat, latLon.lon).then((address) => { + setFavoriteAddress(address ?? ''); + setSearching(false); + }); + }, [latLon]); + + function handleClear() { + setFavoriteAddress(''); + } -export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, setClose, widthDialog }) { return ( - + setFavoriteAddress(e.target.value)} - value={favoriteAddress} - autoFocus - sx={{ - maxWidth: '450px !important', - resize: 'none', - fontFamily: 'Arial', - color: 'black', - fontSize: 20, - ml: '-2px', - borderColor: '#bebdb4', - backgroundColor: 'transparent', - outlineColor: '#757575', - cursor: 'pointer', - '&[disabled]': { border: 'none' }, - mb: '-10px', - pb: '8px', - pt: '8px', + value={searching ? t('web:fav_address_searching') : favoriteAddress} + InputProps={{ + endAdornment: ( + + + + + + ), }} /> - {favoriteAddress && favoriteAddress !== '' && ( - { - if (setClose) { - setClose(false); - } - setFavoriteAddress(''); - }} - > - - - )} - + ); } diff --git a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx index afc763ed66..f675a6842c 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx @@ -1,4 +1,4 @@ -import { ListItemText, TextField } from '@mui/material'; +import { Box, TextField } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import AppContext from '../../../../context/AppContext'; import { getPropsFromSearchResultItem } from '../../../../menu/search/search/SearchResultItem'; @@ -85,12 +85,11 @@ export default function FavoriteName({ }, [ctx.selectedWpt]); return ( - + setFavoriteName(e.target.value)} value={favoriteName} @@ -98,6 +97,6 @@ export default function FavoriteName({ error={favoriteName === '' || nameAlreadyExist} helperText={getErrorText(favoriteName)} /> - + ); } diff --git a/map/src/infoblock/components/favorite/wptEditPanel.module.css b/map/src/infoblock/components/favorite/wptEditPanel.module.css index 29d107d420..9df4f0f090 100644 --- a/map/src/infoblock/components/favorite/wptEditPanel.module.css +++ b/map/src/infoblock/components/favorite/wptEditPanel.module.css @@ -13,7 +13,7 @@ } .fields { - padding: 8px 16px 16px 16px; + padding: 16px; } .actions { diff --git a/map/src/infoblock/components/wpt/WptDetails.jsx b/map/src/infoblock/components/wpt/WptDetails.jsx index 5dc444ec79..ad0959bb8a 100644 --- a/map/src/infoblock/components/wpt/WptDetails.jsx +++ b/map/src/infoblock/components/wpt/WptDetails.jsx @@ -161,6 +161,22 @@ export const ADDRESS_NOT_FOUND = i18n.t('web:no_data'); export const TYPE_NOT_FOUND = 'No type'; export const EMPTY_STRING = ''; +export async function getAddressByLatLon(lat, lon) { + if (lat == null || lon == null) return null; + const response = await apiGet(`${process.env.REACT_APP_ROUTING_API_SITE}/search/get-poi-address`, { + apiCache: true, + params: { lat, lon }, + }); + if (response?.data) { + return response.data + .replace(/ str\./g, '') + .replace(/ city/g, ',') + .replace(/ dist.*/g, ''); + } + + return null; +} + export default function WptDetails({ setOpenWptTab, setShowInfoBlock }) { const ctx = useContext(AppContext); const { t } = useTranslation(); @@ -639,25 +655,8 @@ export default function WptDetails({ setOpenWptTab, setShowInfoBlock }) { return fmt.dateTimeShort(time); } - async function getPoiAddress(wpt) { - if (wpt.latlon?.lat == null || wpt.latlon?.lon == null) { - return null; - } - const response = await apiGet(`${process.env.REACT_APP_ROUTING_API_SITE}/search/get-poi-address`, { - apiCache: true, - params: { - lat: wpt.latlon.lat, - lon: wpt.latlon.lon, - }, - }); - if (response?.data) { - return response.data - .replace(/ str\./g, '') - .replace(/ city/g, ',') - .replace(/ dist.*/g, ''); - } else { - return null; - } + function getPoiAddress(wpt) { + return getAddressByLatLon(wpt.latlon?.lat, wpt.latlon?.lon); } async function getPhotos(wpt) { diff --git a/map/src/resources/translations/en/web-translation.json b/map/src/resources/translations/en/web-translation.json index c0fd2205c3..8dd0402d18 100644 --- a/map/src/resources/translations/en/web-translation.json +++ b/map/src/resources/translations/en/web-translation.json @@ -317,6 +317,8 @@ "rename_folder_already_exists": "Folder already exists.", "fav_name_empty": "Empty name.", "fav_name_already_exists": "This name already exists.", + "fav_address": "Address", + "fav_address_searching": "Searching...", "rename_item_not_found": "Folder or track was not found.", "garmin_sync_title": "Sync your activities", "garmin_sync_description": "Connect your account to sync Garmin Connect™ activities with OsmAnd.", From 3ad136061bab2b80101289547d74b9b60f4a220a Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 24 Apr 2026 14:42:54 +0300 Subject: [PATCH 03/16] Fix paddings between inputs --- .../favorite/structure/FavoriteAddress.jsx | 26 +++++++++++++------ .../favorite/structure/FavoriteName.jsx | 2 ++ .../favorite/wptEditPanel.module.css | 9 +++++++ .../translations/en/web-translation.json | 1 + 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx index 1ce9aa7d79..c52d8382cc 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx @@ -1,5 +1,6 @@ -import { Box, IconButton, InputAdornment, TextField } from '@mui/material'; +import { Box, IconButton, InputAdornment, TextField, Tooltip } from '@mui/material'; import { ReactComponent as CancelIcon } from '../../../../assets/icons/ic_action_cancel.svg'; +import { ReactComponent as LocationIcon } from '../../../../assets/icons/ic_action_location_marker_outlined.svg'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getAddressByLatLon } from '../../wpt/WptDetails'; @@ -11,15 +12,16 @@ export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, w useEffect(() => { if (!latLon?.lat || !latLon?.lon || favoriteAddress) return; + searchAddress(); + }, [latLon]); + + function searchAddress() { + if (!latLon?.lat || !latLon?.lon) return; setSearching(true); getAddressByLatLon(latLon.lat, latLon.lon).then((address) => { setFavoriteAddress(address ?? ''); setSearching(false); }); - }, [latLon]); - - function handleClear() { - setFavoriteAddress(''); } return ( @@ -35,9 +37,17 @@ export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, w InputProps={{ endAdornment: ( - - - + {favoriteAddress ? ( + setFavoriteAddress('')}> + + + ) : ( + + + + + + )} ), }} diff --git a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx index f675a6842c..6658555ad4 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteName.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteName.jsx @@ -3,6 +3,7 @@ import React, { useContext, useEffect, useState } from 'react'; import AppContext from '../../../../context/AppContext'; import { getPropsFromSearchResultItem } from '../../../../menu/search/search/SearchResultItem'; import { useTranslation } from 'react-i18next'; +import styles from '../wptEditPanel.module.css'; export default function FavoriteName({ favoriteName, @@ -96,6 +97,7 @@ export default function FavoriteName({ autoFocus error={favoriteName === '' || nameAlreadyExist} helperText={getErrorText(favoriteName)} + FormHelperTextProps={{ className: styles.helperText }} /> ); diff --git a/map/src/infoblock/components/favorite/wptEditPanel.module.css b/map/src/infoblock/components/favorite/wptEditPanel.module.css index 9df4f0f090..756371f47d 100644 --- a/map/src/infoblock/components/favorite/wptEditPanel.module.css +++ b/map/src/infoblock/components/favorite/wptEditPanel.module.css @@ -16,6 +16,15 @@ padding: 16px; } +.helperText { + height: 16px; + line-height: 16px; + margin: 2px 0 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .actions { display: flex; justify-content: flex-end; diff --git a/map/src/resources/translations/en/web-translation.json b/map/src/resources/translations/en/web-translation.json index 8dd0402d18..c4013029cd 100644 --- a/map/src/resources/translations/en/web-translation.json +++ b/map/src/resources/translations/en/web-translation.json @@ -319,6 +319,7 @@ "fav_name_already_exists": "This name already exists.", "fav_address": "Address", "fav_address_searching": "Searching...", + "fav_address_autofill": "Auto-fill address", "rename_item_not_found": "Folder or track was not found.", "garmin_sync_title": "Sync your activities", "garmin_sync_description": "Connect your account to sync Garmin Connect™ activities with OsmAnd.", From 2919908277758454c9ed55831f98cbaa809f1d2a Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Fri, 24 Apr 2026 14:49:28 +0300 Subject: [PATCH 04/16] Fix css --- .../components/favorite/structure/FavoriteAddress.jsx | 2 ++ map/src/infoblock/components/favorite/wptEditPanel.module.css | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx index c52d8382cc..be13d0ce77 100644 --- a/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx +++ b/map/src/infoblock/components/favorite/structure/FavoriteAddress.jsx @@ -4,6 +4,7 @@ import { ReactComponent as LocationIcon } from '../../../../assets/icons/ic_acti import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getAddressByLatLon } from '../../wpt/WptDetails'; +import styles from '../wptEditPanel.module.css'; export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, widthDialog, latLon }) { const { t } = useTranslation(); @@ -34,6 +35,7 @@ export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, w disabled={searching} onChange={(e) => setFavoriteAddress(e.target.value)} value={searching ? t('web:fav_address_searching') : favoriteAddress} + inputProps={{ className: styles.fieldInput }} InputProps={{ endAdornment: ( diff --git a/map/src/infoblock/components/favorite/wptEditPanel.module.css b/map/src/infoblock/components/favorite/wptEditPanel.module.css index 756371f47d..4178acf006 100644 --- a/map/src/infoblock/components/favorite/wptEditPanel.module.css +++ b/map/src/infoblock/components/favorite/wptEditPanel.module.css @@ -25,6 +25,10 @@ text-overflow: ellipsis; } +.fieldInput { + padding-left: 8px !important; +} + .actions { display: flex; justify-content: flex-end; From 870776106be23ef8573de12ba0dbfa1bc8afc953 Mon Sep 17 00:00:00 2001 From: Kseniia Velychko Date: Tue, 28 Apr 2026 08:50:28 +0300 Subject: [PATCH 05/16] Replace react-swipeable-views with swiper in PhotosModal --- map/package.json | 2 +- map/src/menu/search/explore/PhotosModal.jsx | 237 +- map/src/menu/search/search.module.css | 5 + .../translations/en/web-translation.json | 1 + map/yarn.lock | 4029 ++++++++--------- 5 files changed, 1940 insertions(+), 2334 deletions(-) diff --git a/map/package.json b/map/package.json index 7b3e864252..ec8473427d 100644 --- a/map/package.json +++ b/map/package.json @@ -51,11 +51,11 @@ "react-leaflet-markercluster": "^3.0.0-rc1", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", - "react-swipeable-views": "^0.14.0", "react-use-cookie": "^1.4.0", "react-window": "^1.8.7", "recharts": "^2.1.10", "source-map-explorer": "^2.5.3", + "swiper": "^12.1.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/map/src/menu/search/explore/PhotosModal.jsx b/map/src/menu/search/explore/PhotosModal.jsx index 65ec5cb342..7584cbd67b 100644 --- a/map/src/menu/search/explore/PhotosModal.jsx +++ b/map/src/menu/search/explore/PhotosModal.jsx @@ -1,6 +1,7 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { AppBar, Box, Button, Drawer, IconButton, Toolbar, Typography, Skeleton } from '@mui/material'; -import SwipeableViews from 'react-swipeable-views'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; import { useWindowSize } from '../../../util/hooks/useWindowSize'; import { ReactComponent as BackIcon } from '../../../assets/icons/ic_arrow_back.svg'; import { ReactComponent as BackForward } from '../../../assets/icons/ic_arrow_forward.svg'; @@ -18,20 +19,33 @@ import PropTypes from 'prop-types'; import { fmt } from '../../../util/dateFmt'; import { getPhotoTitle } from '../../../manager/SearchManager'; +const HEADER_HEIGHT = 60; +const LEFT_MARGIN = 423; +const FOOTER_HEIGHT = 88; +const METADATA_MARGIN = 62; + export default function PhotosModal({ photos }) { const ctx = useContext(AppContext); const { t, i18n } = useTranslation(); + const swiperRef = useRef(null); + const [width, height] = useWindowSize(); + const [open, setOpen] = useState(true); const [activeStep, setActiveStep] = useState(ctx.selectedPhotoInd); - const [width, height] = useWindowSize(); const [showInfo, setShowInfo] = useState(false); const [activePhoto, setActivePhoto] = useState(null); - const HEADER_HEIGHT = 60; - const LEFT_MARGIN = 423; - const METADATA_MARGIN = 62; - const FOOTER_HEIGHT = 88; + const handleClose = () => ctx.setSelectedPhotoInd(-1); + + const handleNext = () => ctx.setSelectedPhotoInd(Math.min(activeStep + 1, photos.length - 1)); + + const handleBack = () => ctx.setSelectedPhotoInd(Math.max(activeStep - 1, 0)); + + const handleStepChange = (step) => { + setActiveStep(step); + ctx.setSelectedPhotoInd(step); + }; useEffect(() => { if (ctx.selectedPhotoInd !== -1) { @@ -55,18 +69,6 @@ export default function PhotosModal({ photos }) { } }, [ctx.selectedPhotoInd]); - const handleClose = () => { - ctx.setSelectedPhotoInd(-1); - }; - - const handleNext = () => { - ctx.setSelectedPhotoInd(Math.min(activeStep + 1, photos.length - 1)); - }; - - const handleBack = () => { - ctx.setSelectedPhotoInd(Math.max(activeStep - 1, 0)); - }; - useEffect(() => { const handleKeyDown = (event) => { if (event.key === 'ArrowRight' || event.key === ' ') { @@ -75,90 +77,31 @@ export default function PhotosModal({ photos }) { handleBack(); } }; + globalThis.addEventListener('keydown', handleKeyDown); - window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener('keydown', handleKeyDown); + globalThis.removeEventListener('keydown', handleKeyDown); }; }, [handleNext, handleBack]); - const handleStepChange = (step) => setActiveStep(step); - - function getHeight() { - return height - HEADER_HEIGHT; - } - - function getPhotoHeight() { - return height - HEADER_HEIGHT - FOOTER_HEIGHT; - } + useEffect(() => { + if (swiperRef.current && swiperRef.current.activeIndex !== activeStep) { + swiperRef.current.slideTo(activeStep); + } + }, [activeStep]); - function getWidth() { - return width - LEFT_MARGIN; - } + const getHeight = () => height - HEADER_HEIGHT; + const getWidth = () => width - LEFT_MARGIN; - function needOpenMoreModal(str) { - const fontSize = 16; - const avgCharWidth = fontSize * 0.6; - const textWidth = str.length * avgCharWidth; - const containerWidth = getWidth() - METADATA_MARGIN; - return textWidth > containerWidth; - } + const needOpenMoreModal = (str) => { + const textWidth = str.length * 16 * 0.6; + return textWidth > getWidth() - METADATA_MARGIN; + }; if (!photos || photos.length === 0) { return null; } - function hasFooterInfo(photo) { - return ( - photo?.properties?.date || - photo?.properties?.author || - photo?.properties?.license || - photo?.properties?.description - ); - } - - const formatDate = (dateStr) => { - if (!dateStr) { - return ''; - } - const clean = dateStr?.startsWith('+') ? dateStr.slice(1) : dateStr; - // Format YYYY 2025 -> 2025 - const yearRegex = /^\d{4}$/; - if (yearRegex.test(clean)) { - return clean; - } - // Format YYYY-MM 2025-09 -> September 2025 - const yearMonthRegex = /^\d{4}-\d{2}$/; - if (yearMonthRegex.test(clean)) { - const d = new Date(clean + '-01'); // Add the first day of the month for browser compatibility - return isNaN(d) ? dateStr : fmt.monthYearLong(d); - } - // Format YYYY-MM-DD 2025-09-04 -> 4 September 2025 - const d = new Date(clean); - return isNaN(d) ? dateStr : fmt.dMMMMY(d); - }; - - const parseDescription = (descriptionStr) => { - if (!descriptionStr) { - return ''; - } - try { - const parsed = JSON.parse(descriptionStr); - const currentLang = i18n.language?.split('-')[0] || 'en'; - - if (parsed[currentLang]) { - return parsed[currentLang]; - } else if (parsed['en']) { - return parsed['en']; - } else { - const firstKey = Object.keys(parsed)[0]; - return firstKey ? parsed[firstKey] : descriptionStr; - } - } catch (e) { - return descriptionStr; - } - }; - return ( - Photos + {t('web:photos')} - + (swiperRef.current = swiper)} + onSlideChange={(swiper) => handleStepChange(swiper.activeIndex)} + > {photos.map((photo, index) => ( - + + + ))} - +