diff --git a/web/src/components/MemoEditor/ActionButton/LocationSelector.tsx b/web/src/components/MemoEditor/ActionButton/LocationSelector.tsx index 40dfe2743..b43809b83 100644 --- a/web/src/components/MemoEditor/ActionButton/LocationSelector.tsx +++ b/web/src/components/MemoEditor/ActionButton/LocationSelector.tsx @@ -5,7 +5,9 @@ import toast from "react-hot-toast"; import LeafletMap from "@/components/LeafletMap"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Location } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; @@ -16,9 +18,11 @@ interface Props { } interface State { - initilized: boolean; + initialized: boolean; placeholder: string; position?: LatLng; + latInput: string; + lngInput: string; } interface NominatimRateLimit { @@ -30,9 +34,11 @@ interface NominatimRateLimit { const LocationSelector = (props: Props) => { const t = useTranslate(); const [state, setState] = useState({ - initilized: false, + initialized: false, placeholder: props.location?.placeholder || "", position: props.location ? new LatLng(props.location.latitude, props.location.longitude) : undefined, + latInput: props.location ? String(props.location.latitude) : "", + lngInput: props.location ? String(props.location.longitude) : "", }); const rateLimit = useRef({ lastNominatimFetch: new Date(0), @@ -47,13 +53,15 @@ const LocationSelector = (props: Props) => { ...state, placeholder: props.location?.placeholder || "", position: new LatLng(props.location?.latitude || 0, props.location?.longitude || 0), + latInput: String(props.location?.latitude) || "", + lngInput: String(props.location?.longitude) || "", })); }, [props.location]); useEffect(() => { if (popoverOpen && !props.location) { const handleError = (error: any, errorMessage: string) => { - setState({ ...state, initilized: true }); + setState((prev) => ({ ...prev, initialized: true })); toast.error(errorMessage); console.error(error); }; @@ -63,7 +71,13 @@ const LocationSelector = (props: Props) => { (position) => { const lat = position.coords.latitude; const lng = position.coords.longitude; - setState({ ...state, position: new LatLng(lat, lng), initilized: true }); + setState((prev) => ({ + ...prev, + position: new LatLng(lat, lng), + latInput: String(lat), + lngInput: String(lng), + initialized: true, + })); }, (error) => { handleError(error, "Failed to get current position"); @@ -73,14 +87,21 @@ const LocationSelector = (props: Props) => { handleError("Geolocation is not supported by this browser.", "Geolocation is not supported by this browser."); } } - }, [popoverOpen]); + }, [popoverOpen, props.location]); const updateReverseGeocoding = () => { if (!state.position) { - setState({ ...state, placeholder: "" }); + setState((prev) => ({ ...prev, placeholder: "" })); return; } + const newLat = String(state.position.lat); + const newLng = String(state.position.lng); + if (state.latInput !== newLat || state.lngInput !== newLng) { + setState((prev) => ({ ...prev, latInput: newLat, lngInput: newLng })); + } + + // Fetch reverse geocoding data. fetch(`https://nominatim.openstreetmap.org/reverse?lat=${state.position.lat}&lon=${state.position.lng}&format=json`, { cache: "default", headers: new Headers({ "Cache-Control": "max-age=86400" }), @@ -88,7 +109,7 @@ const LocationSelector = (props: Props) => { .then((response) => response.json()) .then((data) => { if (data && data.display_name) { - setState({ ...state, placeholder: data.display_name }); + setState((prev) => ({ ...prev, placeholder: data.display_name })); } }) .catch((error) => { @@ -110,8 +131,20 @@ const LocationSelector = (props: Props) => { ); }, [state.position]); + // Update position when lat/lng inputs change (if valid numbers) + useEffect(() => { + const lat = parseFloat(state.latInput); + const lng = parseFloat(state.lngInput); + // Validate coordinate ranges: lat must be -90 to 90, lng must be -180 to 180 + if (Number.isFinite(lat) && Number.isFinite(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + if (!state.position || state.position.lat !== lat || state.position.lng !== lng) { + setState((prev) => ({ ...prev, position: new LatLng(lat, lng) })); + } + } + }, [state.latInput, state.lngInput]); + const onPositionChanged = (position: LatLng) => { - setState({ ...state, position }); + setState((prev) => ({ ...prev, position })); }; const removeLocation = (e: React.MouseEvent) => { @@ -148,29 +181,66 @@ const LocationSelector = (props: Props) => { )} - -
- -
-
-
- {state.position && ( -
- [{state.position.lat.toFixed(2)}, {state.position.lng.toFixed(2)}] -
- )} + +
+
+ +
+
+
+
+ + setState((prev) => ({ ...prev, latInput: e.target.value }))} + className="h-9" + /> +
+
+ setState((state) => ({ ...state, placeholder: e.target.value }))} + id="memo-location-lng" + placeholder="Lng" + type="number" + step="any" + min="-180" + max="180" + value={state.lngInput} + onChange={(e) => setState((prev) => ({ ...prev, lngInput: e.target.value }))} + className="h-9" />
+
+ +