feat: require add mode before map click opens modal

This commit is contained in:
2025-10-01 12:19:01 +08:00
parent c1fc26df03
commit c6b1cc23da
4 changed files with 59 additions and 25 deletions

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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",

View File

@@ -14,7 +14,7 @@
}, },
"sidebar": { "sidebar": {
"title": "我的旅程", "title": "我的旅程",
"subtitle": "點擊地圖任意位置開始記錄", "subtitle": "先點右下角的+按鈕,再點地圖任一處開始記錄",
"add": "新增足跡", "add": "新增足跡",
"drawerTitleCreate": "新增足跡", "drawerTitleCreate": "新增足跡",
"drawerTitleEdit": "編輯足跡", "drawerTitleEdit": "編輯足跡",