feat: require add mode before map click opens modal
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { MapContainer, Marker, Popup, TileLayer, useMapEvents } from 'react-leaflet';
|
import { MapContainer, Marker, Popup, TileLayer, useMapEvents } from 'react-leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { LeafletMouseEvent } from 'leaflet';
|
import type { LeafletMouseEvent } from 'leaflet';
|
||||||
import { ensureLeafletConfig } from './leafletConfig';
|
import { ensureLeafletConfig } from './leafletConfig';
|
||||||
import { useVisitModalStore } from '../../state/useVisitModalStore';
|
import { useVisitModalStore } from '../../state/useVisitModalStore';
|
||||||
@@ -14,13 +14,20 @@ import AddIcon from '@mui/icons-material/Add';
|
|||||||
const INITIAL_POSITION: [number, number] = [20, 0];
|
const INITIAL_POSITION: [number, number] = [20, 0];
|
||||||
const INITIAL_ZOOM = 2;
|
const INITIAL_ZOOM = 2;
|
||||||
|
|
||||||
function MapEvents() {
|
interface MapEventsProps {
|
||||||
const openForCreate = useVisitModalStore((state) => state.openForCreate);
|
enabled: boolean;
|
||||||
|
onSelect: (location: { lat: number; lng: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapEvents({ enabled, onSelect }: MapEventsProps) {
|
||||||
useMapEvents({
|
useMapEvents({
|
||||||
click: (event: LeafletMouseEvent) => {
|
click: (event: LeafletMouseEvent) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { lat, lng } = event.latlng;
|
const { lat, lng } = event.latlng;
|
||||||
openForCreate({ lat, lng });
|
onSelect({ lat, lng });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,6 +50,25 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mapRef = useRef<LeafletMap | null>(null);
|
const mapRef = useRef<LeafletMap | null>(null);
|
||||||
useResizeMap(mapRef, [sidebarOpen]);
|
useResizeMap(mapRef, [sidebarOpen]);
|
||||||
|
const [isAwaitingLocation, setIsAwaitingLocation] = useState(false);
|
||||||
|
|
||||||
|
const handleMapSelection = useCallback(
|
||||||
|
({ lat, lng }: { lat: number; lng: number }) => {
|
||||||
|
setIsAwaitingLocation(false);
|
||||||
|
|
||||||
|
if (onTriggerCreate) {
|
||||||
|
onTriggerCreate({ lat, lng });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openForCreate({ lat, lng });
|
||||||
|
},
|
||||||
|
[onTriggerCreate, openForCreate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFabClick = useCallback(() => {
|
||||||
|
setIsAwaitingLocation((previous) => !previous);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', height: '100%', width: '100%' }}>
|
<Box sx={{ position: 'relative', height: '100%', width: '100%' }}>
|
||||||
@@ -51,7 +77,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
|
|||||||
mapRef.current = mapInstance;
|
mapRef.current = mapInstance;
|
||||||
mapInstance.invalidateSize();
|
mapInstance.invalidateSize();
|
||||||
}}
|
}}
|
||||||
className="h-full w-full z-0"
|
className={`h-full w-full z-0 ${isAwaitingLocation ? 'cursor-crosshair' : ''}`}
|
||||||
center={INITIAL_POSITION}
|
center={INITIAL_POSITION}
|
||||||
zoom={INITIAL_ZOOM}
|
zoom={INITIAL_ZOOM}
|
||||||
scrollWheelZoom
|
scrollWheelZoom
|
||||||
@@ -60,7 +86,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
|
|||||||
attribution='\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution='\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
<MapEvents />
|
<MapEvents enabled={isAwaitingLocation} onSelect={handleMapSelection} />
|
||||||
{visits.map((visit) => (
|
{visits.map((visit) => (
|
||||||
<Marker key={visit.id} position={[visit.location.lat, visit.location.lng]}>
|
<Marker key={visit.id} position={[visit.location.lat, visit.location.lng]}>
|
||||||
<Popup>
|
<Popup>
|
||||||
@@ -78,9 +104,10 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
|
|||||||
</MapContainer>
|
</MapContainer>
|
||||||
<Tooltip title={t('sidebar.add')} placement="left">
|
<Tooltip title={t('sidebar.add')} placement="left">
|
||||||
<Fab
|
<Fab
|
||||||
color="primary"
|
color={isAwaitingLocation ? 'secondary' : 'primary'}
|
||||||
sx={{ position: 'absolute', bottom: 24, right: 24, zIndex: 1000 }}
|
sx={{ position: 'absolute', bottom: 24, right: 24, zIndex: 1000 }}
|
||||||
onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())}
|
onClick={handleFabClick}
|
||||||
|
aria-pressed={isAwaitingLocation}
|
||||||
>
|
>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card, CardContent, Grid, Paper, Typography } from '@mui/material';
|
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||||
import { useVisitsQuery } from '../hooks/useVisitQueries';
|
import { useVisitsQuery } from '../hooks/useVisitQueries';
|
||||||
|
|
||||||
export function VisitSummary() {
|
export function VisitSummary() {
|
||||||
@@ -22,25 +22,32 @@ export function VisitSummary() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outlined">
|
<Card variant="outlined" sx={{ borderRadius: 2 }}>
|
||||||
<CardContent>
|
<CardContent sx={{ py: 1.5, px: 2 }}>
|
||||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
{t('summary.title')}
|
{t('summary.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={2}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
overflowX: 'auto',
|
||||||
|
pb: 0.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
{summaryItems.map((item) => (
|
{summaryItems.map((item) => (
|
||||||
<Grid item xs={6} key={item.label}>
|
<Box key={item.label} sx={{ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Paper elevation={0} sx={{ p: 2, bgcolor: 'grey.100', textAlign: 'center' }}>
|
<Typography variant="caption" color="text.secondary">
|
||||||
<Typography variant="overline" display="block" color="text.secondary">
|
{item.label}
|
||||||
{item.label}
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="h6" fontWeight={700} color="text.primary">
|
||||||
<Typography variant="h5" fontWeight={700} color="text.primary">
|
{item.value}
|
||||||
{item.value}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "My Journeys",
|
"title": "My Journeys",
|
||||||
"subtitle": "Click anywhere on the map to start recording",
|
"subtitle": "Tap the + button, then click the map to start recording",
|
||||||
"add": "Add Visit",
|
"add": "Add Visit",
|
||||||
"drawerTitleCreate": "Add Visit",
|
"drawerTitleCreate": "Add Visit",
|
||||||
"drawerTitleEdit": "Edit Visit",
|
"drawerTitleEdit": "Edit Visit",
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "我的旅程",
|
"title": "我的旅程",
|
||||||
"subtitle": "點擊地圖任意位置開始記錄",
|
"subtitle": "先點右下角的+按鈕,再點地圖任一處開始記錄",
|
||||||
"add": "新增足跡",
|
"add": "新增足跡",
|
||||||
"drawerTitleCreate": "新增足跡",
|
"drawerTitleCreate": "新增足跡",
|
||||||
"drawerTitleEdit": "編輯足跡",
|
"drawerTitleEdit": "編輯足跡",
|
||||||
|
Reference in New Issue
Block a user