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 'leaflet/dist/leaflet.css';
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { LeafletMouseEvent } from 'leaflet';
import { ensureLeafletConfig } from './leafletConfig';
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_ZOOM = 2;
function MapEvents() {
const openForCreate = useVisitModalStore((state) => state.openForCreate);
interface MapEventsProps {
enabled: boolean;
onSelect: (location: { lat: number; lng: number }) => void;
}
function MapEvents({ enabled, onSelect }: MapEventsProps) {
useMapEvents({
click: (event: LeafletMouseEvent) => {
if (!enabled) {
return;
}
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 mapRef = useRef<LeafletMap | null>(null);
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 (
<Box sx={{ position: 'relative', height: '100%', width: '100%' }}>
@@ -51,7 +77,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
mapRef.current = mapInstance;
mapInstance.invalidateSize();
}}
className="h-full w-full z-0"
className={`h-full w-full z-0 ${isAwaitingLocation ? 'cursor-crosshair' : ''}`}
center={INITIAL_POSITION}
zoom={INITIAL_ZOOM}
scrollWheelZoom
@@ -60,7 +86,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
attribution='\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapEvents />
<MapEvents enabled={isAwaitingLocation} onSelect={handleMapSelection} />
{visits.map((visit) => (
<Marker key={visit.id} position={[visit.location.lat, visit.location.lng]}>
<Popup>
@@ -78,9 +104,10 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
</MapContainer>
<Tooltip title={t('sidebar.add')} placement="left">
<Fab
color="primary"
color={isAwaitingLocation ? 'secondary' : 'primary'}
sx={{ position: 'absolute', bottom: 24, right: 24, zIndex: 1000 }}
onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())}
onClick={handleFabClick}
aria-pressed={isAwaitingLocation}
>
<AddIcon />
</Fab>

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
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';
export function VisitSummary() {
@@ -22,25 +22,32 @@ export function VisitSummary() {
];
return (
<Card variant="outlined">
<CardContent>
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardContent sx={{ py: 1.5, px: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('summary.title')}
</Typography>
<Grid container spacing={2}>
<Box
sx={{
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: 2,
overflowX: 'auto',
pb: 0.5
}}
>
{summaryItems.map((item) => (
<Grid item xs={6} key={item.label}>
<Paper elevation={0} sx={{ p: 2, bgcolor: 'grey.100', textAlign: 'center' }}>
<Typography variant="overline" display="block" color="text.secondary">
{item.label}
</Typography>
<Typography variant="h5" fontWeight={700} color="text.primary">
{item.value}
</Typography>
</Paper>
</Grid>
<Box key={item.label} sx={{ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary">
{item.label}
</Typography>
<Typography variant="h6" fontWeight={700} color="text.primary">
{item.value}
</Typography>
</Box>
))}
</Grid>
</Box>
</CardContent>
</Card>
);

View File

@@ -14,7 +14,7 @@
},
"sidebar": {
"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",
"drawerTitleCreate": "Add Visit",
"drawerTitleEdit": "Edit Visit",

View File

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