diff --git a/map/src/App.js b/map/src/App.js index 367a1eee1..1e5e2c516 100644 --- a/map/src/App.js +++ b/map/src/App.js @@ -1,5 +1,5 @@ -import { createContext, React, useCallback, useContext, useState } from 'react'; -import { BrowserRouter, Route, Routes, useNavigate } from 'react-router-dom'; +import { createContext, React, useCallback, useContext, useMemo, useState } from 'react'; +import { createBrowserRouter, Outlet, RouterProvider, useNavigate } from 'react-router-dom'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import GlobalFrame from './frame/GlobalFrame'; import { AppContextProvider } from './context/AppContext'; @@ -106,71 +106,110 @@ const App = () => { } } + const router = useMemo( + () => + createBrowserRouter( + [ + { + path: '/', + element: ( + <> + + + + + ), + children: [ + { + path: MAIN_URL, + element: , + children: [ + { + path: LOGIN_URL, + element: , + children: [ + { + path: PURCHASES_URL, + element: , + children: [{ path: ':key', element: }], + }, + { path: GARMIN_URL, element: }, + ], + }, + { + path: DELETE_ACCOUNT_URL, + element: , + }, + { + path: SEARCH_URL, + element: , + children: [ + { path: EXPLORE_URL, element: }, + { path: POI_CATEGORIES_URL, element: }, + { path: SEARCH_RESULT_URL, element: }, + ], + }, + { path: CONFIGURE_URL, element: }, + { + path: WEATHER_URL, + element: , + children: [{ path: WEATHER_FORECAST_URL, element: }], + }, + { + path: TRACKS_URL, + element: , + children: [ + { + path: INFO_MENU_URL + ':filename', + element: , + children: [{ path: SHARE_MENU_URL, element: }], + }, + ], + }, + { path: VISIBLE_TRACKS_URL, element: }, + { + path: FAVORITES_URL, + element: , + children: [ + { + path: INFO_MENU_URL + ':favgroup/:favname', + element: , + }, + { + path: INFO_MENU_URL + ':filename' + '/' + SHARE_MENU_URL, + element: , + }, + ], + }, + { path: NAVIGATE_URL, element: }, + { path: PLANROUTE_URL, element: }, + { path: TRAVEL_URL, element: }, + { path: SETTINGS_URL, element: }, + { path: SHARE_FILE_URL, element: }, + { path: TRACK_ANALYZER_URL, element: }, + { path: POI_URL, element: }, + { path: STOP_URL, element: }, + ], + }, + { path: PRICING_URL, element: }, + ], + }, + ], + { + // Enable React Router v7 features: concurrent transitions and correct relative splat path resolution + future: { v7_startTransition: true, v7_relativeSplatPath: true }, + } + ), + [resetKey] + ); + return ( - - - - - }> - }> - }> - }> - - } /> - - } - /> - }> - }> - }> - }> - - }> - }> - } - > - - }> - }> - } /> - - - }> - }> - } - /> - } - /> - - }> - }> - }> - }> - }> - }> - }> - }> - - }> - - + diff --git a/map/src/context/AppContext.js b/map/src/context/AppContext.js index c82bdb237..ac9835834 100644 --- a/map/src/context/AppContext.js +++ b/map/src/context/AppContext.js @@ -207,6 +207,9 @@ export const AppContextProvider = (props) => { add: false, location: null, }); + + const [exitGuards, setExitGuards] = useState({}); + const [processingGroups, setProcessingGroups] = useState(false); const [favLoading, setFavLoading] = useState(false); const [removeFavGroup, setRemoveFavGroup] = useState(null); @@ -555,6 +558,8 @@ export const AppContextProvider = (props) => { setFavorites, addFavorite, setAddFavorite, + exitGuards, + setExitGuards, localTracks, setLocalTracks, currentObjectType, diff --git a/map/src/dialogs/dialog.module.css b/map/src/dialogs/dialog.module.css index 3533cbd79..f247ddce8 100644 --- a/map/src/dialogs/dialog.module.css +++ b/map/src/dialogs/dialog.module.css @@ -37,6 +37,15 @@ letter-spacing: 0.28px !important; text-transform: uppercase !important; } +.contentText { + font-size: 14px !important; + font-weight: 400 !important; + line-height: 20px !important; + letter-spacing: 0.25px !important; + color: var(--text-secondary) !important; + align-self: stretch !important; + padding: 12px 0 !important; +} /* 280px (.title width) + 24px * 2 (left/right padding from .title) */ .dialog :global(.MuiPaper-root) { max-width: 328px !important; diff --git a/map/src/dialogs/favorites/ExitWithoutSavingDialog.jsx b/map/src/dialogs/favorites/ExitWithoutSavingDialog.jsx new file mode 100644 index 000000000..84cddeac4 --- /dev/null +++ b/map/src/dialogs/favorites/ExitWithoutSavingDialog.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import dialogStyles from '../dialog.module.css'; + +export default function ExitWithoutSavingDialog({ open, onKeepEditing, onExit }) { + const { t } = useTranslation(); + + return ( + + {t('web:exit_without_saving')} + + + {t('web:all_changes_will_be_lost')} + + + + + + + + ); +} diff --git a/map/src/frame/components/btns/buttons.module.css b/map/src/frame/components/btns/buttons.module.css index c6130d5d7..cb02c4372 100644 --- a/map/src/frame/components/btns/buttons.module.css +++ b/map/src/frame/components/btns/buttons.module.css @@ -91,7 +91,7 @@ } .primaryButton { display: flex !important; - padding: 6px 0px !important; + padding: 6px 12px !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; diff --git a/map/src/frame/components/items/ChevronItem.jsx b/map/src/frame/components/items/ChevronItem.jsx new file mode 100644 index 000000000..b8d0e2f6e --- /dev/null +++ b/map/src/frame/components/items/ChevronItem.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { ListItemIcon, ListItemText, MenuItem, Typography } from '@mui/material'; +import { ReactComponent as ChevronIcon } from '../../../assets/icons/ic_action_arrow_up.svg'; +import styles from './items.module.css'; + +export default function ChevronItem({ id, icon = null, title, titleProps, value, onClick, disabled = false }) { + return ( + + {icon && {icon}} + + + {title} + + +
+ {value !== undefined && {value}} + +
+
+ ); +} diff --git a/map/src/frame/components/items/SelectItemRadio.jsx b/map/src/frame/components/items/SelectItemRadio.jsx index e79296272..59cbd2701 100644 --- a/map/src/frame/components/items/SelectItemRadio.jsx +++ b/map/src/frame/components/items/SelectItemRadio.jsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Box, ListItemText, MenuItem, Radio, Typography } from '@mui/material'; +import { Box, ListItemText, MenuItem, Typography } from '@mui/material'; +import SmallRadio from './SmallRadio'; import styles from './items.module.css'; import DividerWithMargin from '../dividers/DividerWithMargin'; @@ -34,7 +35,7 @@ export default function SelectItemRadio({ {title} - + diff --git a/map/src/frame/components/items/SmallRadio.jsx b/map/src/frame/components/items/SmallRadio.jsx new file mode 100644 index 000000000..c98ab7fc5 --- /dev/null +++ b/map/src/frame/components/items/SmallRadio.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Radio } from '@mui/material'; + +export default function SmallRadio({ checked, onChange, onClick, className }) { + return ( + + ); +} diff --git a/map/src/frame/components/items/items.module.css b/map/src/frame/components/items/items.module.css index 3cf51af55..8dda586a2 100644 --- a/map/src/frame/components/items/items.module.css +++ b/map/src/frame/components/items/items.module.css @@ -4,6 +4,15 @@ gap: 24px !important; min-height: 48px !important; } +.itemChevron { + padding-right: 12px !important; +} +.itemChevronRight { + display: flex !important; + align-items: center !important; + gap: 6px !important; + flex-shrink: 0 !important; +} .mainText { color: var(--text-primary) !important; font-size: 16px !important; @@ -136,7 +145,7 @@ .sectionRow { width: 360px !important; min-height: 48px !important; - padding: 12px 20px !important; + padding: 12px 12px 12px 16px !important; display: flex !important; align-items: center !important; justify-content: space-between !important; diff --git a/map/src/infoblock/components/InformationBlock.jsx b/map/src/infoblock/components/InformationBlock.jsx index cfc43c09c..8a60ea9ad 100644 --- a/map/src/infoblock/components/InformationBlock.jsx +++ b/map/src/infoblock/components/InformationBlock.jsx @@ -101,10 +101,20 @@ export default function InformationBlock({ // Close WptEditPanel when the user navigates to another object or switches context useEffect(() => { - ctx.setAddFavorite((prev) => { - if (!prev?.location && !prev?.editWpt) return prev; - return { ...prev, add: false, location: null, editTrack: false, editWpt: null, previewAppearance: null }; - }); + const close = () => + ctx.setAddFavorite((prev) => { + if (!prev?.location && !prev?.editWpt) return prev; + return { + ...prev, + add: false, + location: null, + editTrack: false, + editWpt: null, + previewAppearance: null, + }; + }); + if (ctx.exitGuards.wptEdit?.hasChanges) return; + close(); }, [ctx.selectedWpt, ctx.currentObjectType]); useEffect(() => { diff --git a/map/src/infoblock/components/favorite/AddFolderDialog.jsx b/map/src/infoblock/components/favorite/AddFolderDialog.jsx index cf41df9d7..ef1feae07 100644 --- a/map/src/infoblock/components/favorite/AddFolderDialog.jsx +++ b/map/src/infoblock/components/favorite/AddFolderDialog.jsx @@ -1,4 +1,5 @@ -import React, { useContext, useState, useMemo } from 'react'; +import React, { useContext, useState, useMemo, useRef } from 'react'; +import ReactDOM from 'react-dom'; import { Box, Button, @@ -11,8 +12,7 @@ import { List, ListItemButton, ListItemText, - Popover, - Radio, + Paper, TextField, } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; @@ -21,6 +21,7 @@ import AppContext from '../../../context/AppContext'; import MarkerOptions from '../../../map/markers/MarkerOptions'; import FavoritesManager, { decodeGroupNameFromFile, saveFavoriteGroup } from '../../../manager/FavoritesManager'; import { sanitizedFileName } from '../../../util/Utils'; +import SmallRadio from '../../../frame/components/items/SmallRadio'; import dialogStyles from '../../../dialogs/dialog.module.css'; import itemStyles from '../../../frame/components/items/items.module.css'; import styles from './addFolderDialog.module.css'; @@ -32,18 +33,20 @@ export default function AddFolderDialog({ dialogOpen, setDialogOpen, parentGroup const { t } = useTranslation(); const groups = ctx.favorites.groups; - const defaultGroup = groups?.find((g) => g.name === FavoritesManager.DEFAULT_GROUP_NAME) ?? null; const [folderName, setFolderName] = useState(''); const [folderNameError, setFolderNameError] = useState(''); - const [selectedParent, setSelectedParent] = useState(parentGroup ?? defaultGroup); - const [locationAnchorEl, setLocationAnchorEl] = useState(null); + const [selectedParent, setSelectedParent] = useState(parentGroup ?? null); + const [foldersDropdownOpen, setFoldersDropdownOpen] = useState(false); + const foldersDropdownRef = useRef(null); const [process, setProcess] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); - const flatGroups = useMemo(() => groupTreeToList(groupTree ?? []), [groupTree]); + const flatGroups = useMemo(() => { + const topLevelItem = { group: null, displayName: t('web:fav_top_level'), level: 0, fullName: null }; + return [topLevelItem, ...groupTreeToList(groupTree ?? [])]; + }, [groupTree, t]); - const locationOpen = Boolean(locationAnchorEl); const parentDisplayName = getDisplayName(selectedParent, t); const canSave = folderName.trim() !== '' && !process; @@ -130,88 +133,41 @@ export default function AddFolderDialog({ dialogOpen, setDialogOpen, parentGroup error={folderNameError !== ''} helperText={folderNameError || undefined} /> - setLocationAnchorEl(locationAnchorEl ? null : e.currentTarget)} - inputProps={{ readOnly: true, style: { cursor: 'pointer' } }} - InputLabelProps={{ shrink: true }} - InputProps={{ - endAdornment: ( - - ), - }} - className={styles.locationField} - /> - setLocationAnchorEl(null)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} - transformOrigin={{ vertical: 'top', horizontal: 'left' }} - disableAutoFocus - disableEnforceFocus - disablePortal - transitionDuration={0} - slotProps={{ - paper: { - sx: { - width: locationAnchorEl?.offsetWidth, - maxHeight: 380, - mt: '10px', - }, - }, - }} - > - - {flatGroups.map((item, idx) => { - const effectiveGroup = item.group ?? { name: item.fullName }; - const isSelected = - selectedParent?.name === item.fullName || - (!selectedParent && item.fullName === FavoritesManager.DEFAULT_GROUP_NAME); - const showDivider = idx > 0 && item.level === 0; - const prefix = item.level >= 1 ? '↳ ' : ''; - - return ( - - {showDivider && } - { - setSelectedParent(effectiveGroup); - setLocationAnchorEl(null); - }} - > - - - { - setSelectedParent(effectiveGroup); - setLocationAnchorEl(null); - }} - onClick={(e) => e.stopPropagation()} - /> - - - - ); - })} - - + + setFoldersDropdownOpen((prev) => !prev)} + inputProps={{ readOnly: true, style: { cursor: 'pointer' } }} + InputLabelProps={{ shrink: true }} + InputProps={{ + endAdornment: ( + + ), + }} + /> + {foldersDropdownOpen && ( + { + setSelectedParent(group); + setFoldersDropdownOpen(false); + }} + onClose={() => setFoldersDropdownOpen(false)} + /> + )} +