Added OpenStreetMap / Leaflet

This commit is contained in:
knom
2024-12-18 09:59:52 +01:00
parent 7639de9e45
commit 99659f36f5
6 changed files with 14575 additions and 13 deletions
+14431
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -39,9 +39,12 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"compatibility": "^1.0.1",
"cookie": "^0.6.0",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.33.0",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility":"0.1.2",
"lucide-react": "^0.428.0",
"next": "14.2.5",
"next-themes": "^0.3.0",
@@ -52,6 +55,7 @@
"react-day-picker": "9.0.8",
"react-dom": "^18",
"react-grid-gallery": "^1.0.1",
"react-leaflet": "^4.2.1",
"react-query": "^3.39.3",
"recharts": "^2.12.7",
"sass": "^1.77.8",
@@ -66,6 +70,7 @@
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.1",
"@types/jsonwebtoken": "^9.0.6",
"@types/leaflet":"^1.9.15",
"@types/node": "^20",
"@types/pg": "^8.11.6",
"@types/react": "^18",
@@ -0,0 +1,36 @@
import L, { Marker as LeafletMarker } from 'leaflet';
import { Marker, Popup } from 'react-leaflet';
import { useRef } from 'react';
import { LatLngExpression } from 'leaflet';
interface CustomMarkerProps {
position: LatLngExpression;
}
export default function CustomMarker({ position } : CustomMarkerProps) {
const markerRef = useRef<LeafletMarker|null>(null);
return (
<Marker
position={position}
ref={markerRef}
eventHandlers={{
mouseover: () => {
if (markerRef.current) {
markerRef.current.openPopup();
}
},
mouseout: () => {
if (markerRef.current) {
markerRef.current.closePopup();
}
},
}}
>
<Popup>
Position: {L.latLng(position).toString()}
</Popup>
</Marker>
);
}
@@ -0,0 +1,63 @@
"use client";
// IMPORTANT: the order matters!
import "leaflet/dist/leaflet.css";
import L, { LatLngExpression } from 'leaflet';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package
import 'leaflet-defaulticon-compatibility';
import { MapContainer, Marker, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
import { useEffect, useRef, useState } from "react";
import { IPlace } from "@/types/common";
import CustomMarker from "./CustomMarker";
interface MapComponentProps {
location: IPlace;
onLocationChange: (place: IPlace) => void;
}
interface MapMouseHandlerProps {
onMouseDown: (coords: LatLngExpression) => void;
}
const MapMouseHandler = ({ onMouseDown }: MapMouseHandlerProps) => {
useMapEvents({
// Handle mouse events
mousedown: (e) => {
onMouseDown(e.latlng);
},
});
return null;
};
export default function Map({ location, onLocationChange }: MapComponentProps) {
const [position, setPosition] = useState<LatLngExpression>([location.latitude, location.longitude]);
const handleClick = (coords: LatLngExpression) => {
const normalized = L.latLng(coords);
setPosition(coords);
onLocationChange({
latitude: normalized.lat,
longitude: normalized.lng,
name: ""
});
};
return (
<MapContainer
center={position}
zoom={14}
scrollWheelZoom={true}
style={{ width: '500px', height: '400px' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapMouseHandler onMouseDown={handleClick} />
<CustomMarker position={position} />
</MapContainer>
);
}
@@ -13,6 +13,11 @@ import React, { useState } from "react";
import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd";
import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong";
import { MapPinCheck } from "lucide-react";
import dynamic from "next/dynamic";
const LazyMap = dynamic(() => import("./Map"), {
ssr: false
});
interface ITagMissingLocationDialogProps {
onSubmit: (place: IPlace) => Promise<any>;
@@ -22,6 +27,11 @@ export default function TagMissingLocationDialog({
}: ITagMissingLocationDialogProps) {
const [open, setOpen] = useState(false);
const [mapPosition,setMapPosition] = useState<IPlace>({
latitude: 48.0,
longitude: 16.0,
name: "home1"
});
return (
@@ -43,18 +53,18 @@ export default function TagMissingLocationDialog({
Latitude and Longitude
</TabsTrigger>
<TabsTrigger value="google">Google Maps</TabsTrigger>
<TabsTrigger value="maps">Map</TabsTrigger>
</TabsList>
<TabsContent value="search">
<TagMissingLocationSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} />
</TabsContent>
<TabsContent value="latlong">
<TagMissingLocationSearchLatLong onSubmit={onSubmit} onOpenChange={setOpen} />
<TagMissingLocationSearchLatLong onSubmit={onSubmit}
onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
</TabsContent>
<TabsContent value="google">
<TabsContent value="maps">
<div className="py-10 flex flex-col gap-6 items-center">
<MapPinCheck size={48} className="text-gray-500"/>
<p>Google Maps Coming Soon...</p>
<LazyMap location={mapPosition} onLocationChange={setMapPosition} />
</div>
</TabsContent>
</Tabs>
@@ -8,25 +8,30 @@ import { useToast } from "@/components/ui/use-toast";
import { searchPlaces } from "@/handlers/api/common.handler";
import { cn } from "@/lib/utils";
import { IPlace } from "@/types/common";
import clsx from "clsx";
import { Check } from "lucide-react";
import React, { useMemo, useRef, useState } from "react";
interface TagMissingLocationSearchLatLongProps {
onSubmit: (place: IPlace) => Promise<any>;
onOpenChange: (open: boolean) => void;
location: IPlace;
onLocationChange: (place: IPlace) => void;
}
export default function TagMissingLocationSearchLatLong(
{
onSubmit,
onOpenChange
onOpenChange,
location,
onLocationChange
}: TagMissingLocationSearchLatLongProps
) {
const { toast } = useToast();
const [formData, setFormData] = useState<IPlace>({
latitude: 0,
longitude: 0,
name: "",
latitude: location.latitude,
longitude: location.longitude,
name: location.name,
});
const [submitting, setSubmitting] = useState(false);
@@ -41,7 +46,8 @@ export default function TagMissingLocationSearchLatLong(
}, [formData]);
const handleChange = (key: string, value: string) => {
if (value.includes(",")) {
// if somebody pastes a combination of "lat, long"
if (key === "latitude" && value.includes(",")) {
const latLong = value.split(",");
if (latLong.length === 2) {
setFormData({
@@ -51,7 +57,6 @@ export default function TagMissingLocationSearchLatLong(
});
}
} else {
setFormData({
...formData,
[key]: value,
@@ -92,6 +97,16 @@ export default function TagMissingLocationSearchLatLong(
};
const handleBlur = () => {
const place :IPlace = {
name: formData.name,
latitude: parseFloat(formData.latitude as unknown as string),
longitude: parseFloat(formData.longitude as unknown as string)
};
onLocationChange(place);
}
return (
<div className="flex flex-col gap-4 py-4 px-2">
<div className="flex items-start gap-2">
@@ -103,6 +118,7 @@ export default function TagMissingLocationSearchLatLong(
onChange={(e) => {
handleChange("latitude", e.target.value);
}}
onBlur={(e) => handleBlur()}
/>
</div>
<div className="flex flex-col gap-2">
@@ -115,10 +131,11 @@ export default function TagMissingLocationSearchLatLong(
onChange={(e) => {
handleChange("longitude", e.target.value);
}}
onBlur={(e) => handleBlur()}
/>
</div>
</div>
<div className="self-end">
<Button
variant="outline"