Merge pull request #92 from knom/feature-OSM-search
Feature: Added OpenStreetMap Search (Missing-locations)
This commit is contained in:
+15
-14
@@ -12,6 +12,7 @@ import { IPlace } from "@/types/common";
|
||||
import React, { useState } from "react";
|
||||
import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd";
|
||||
import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong";
|
||||
import TagMissingLocationOSMSearchAndAdd from "./TagMissingLocationOSMSearchAndAdd";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
|
||||
|
||||
@@ -25,15 +26,14 @@ interface ITagMissingLocationDialogProps {
|
||||
export default function TagMissingLocationDialog({
|
||||
onSubmit,
|
||||
}: ITagMissingLocationDialogProps) {
|
||||
const {selectedIds} = useMissingLocationContext();
|
||||
const { selectedIds } = useMissingLocationContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mapPosition,setMapPosition] = useState<IPlace>({
|
||||
latitude: 48.0,
|
||||
longitude: 16.0,
|
||||
name: "home"
|
||||
const [mapPosition, setMapPosition] = useState<IPlace>({
|
||||
latitude: 48.210033,
|
||||
longitude: 16.363449,
|
||||
name: "Vienna, Austria"
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild >
|
||||
@@ -46,20 +46,21 @@ export default function TagMissingLocationDialog({
|
||||
Tagging a location will add the location to the selected assets.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="search">
|
||||
<TabsList className="flex justify-center">
|
||||
<TabsTrigger value="search">Search and Pick</TabsTrigger>
|
||||
<TabsTrigger value="latlong">
|
||||
Lat & Long
|
||||
</TabsTrigger>
|
||||
<Tabs defaultValue="search" className="border rounded-lg">
|
||||
<TabsList className="flex justify-between">
|
||||
<TabsTrigger value="search">Immich Search</TabsTrigger>
|
||||
<TabsTrigger value="searchOsm">OSM Search</TabsTrigger>
|
||||
<TabsTrigger value="latlong">Lat & Long</TabsTrigger>
|
||||
<TabsTrigger value="maps">Map</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="search">
|
||||
<TagMissingLocationSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} />
|
||||
</TabsContent>
|
||||
<TabsContent value="searchOsm">
|
||||
<TagMissingLocationOSMSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
|
||||
</TabsContent>
|
||||
<TabsContent value="latlong">
|
||||
<TagMissingLocationSearchLatLong onSubmit={onSubmit}
|
||||
onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
|
||||
<TagMissingLocationSearchLatLong onSubmit={onSubmit} onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
|
||||
</TabsContent>
|
||||
<TabsContent value="maps">
|
||||
<div className="flex flex-col gap-6 items-center ">
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import ErrorBlock from "@/components/shared/ErrorBlock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Loader from "@/components/ui/loader";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IPlace } from "@/types/common";
|
||||
import axios from "axios";
|
||||
import { Check } from "lucide-react";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
interface ITagMissingLocationSearchAndAddProps {
|
||||
onSubmit: (place: IPlace) => Promise<any>;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
location: IPlace;
|
||||
onLocationChange: (place: IPlace) => void;
|
||||
}
|
||||
|
||||
export default function TagMissingLocationOSMSearchAndAdd(
|
||||
{
|
||||
onSubmit,
|
||||
onOpenChange,
|
||||
location,
|
||||
onLocationChange
|
||||
}: ITagMissingLocationSearchAndAddProps
|
||||
) {
|
||||
const { toast } = useToast();
|
||||
const [searchedPlaces, setSearchedPlaces] = useState<IPlace[] | null>(null);
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [selectedPlace, setSelectedPlace] = useState<IPlace | null>(null);
|
||||
|
||||
const searchPlaces = async (value: string) => {
|
||||
try {
|
||||
const response = await axios.get("https://nominatim.openstreetmap.org/search", {
|
||||
params: {
|
||||
q: value,
|
||||
format: "json", // Response in JSON format
|
||||
addressdetails: 1, // Include address details
|
||||
limit: 5, // Limit the results to 1
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.length > 0) {
|
||||
const data = (response.data) as Array<any>;
|
||||
|
||||
const places = data.map((result): IPlace => ({
|
||||
name: result.display_name as string, // Full address or place name
|
||||
latitude: parseFloat(result.lat),
|
||||
longitude: parseFloat(result.lon),
|
||||
}));
|
||||
|
||||
return places;
|
||||
} else {
|
||||
console.log("No results found for the location.");
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching coordinates:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
if (searchTimer.current) {
|
||||
clearTimeout(searchTimer.current);
|
||||
}
|
||||
|
||||
searchTimer.current = setTimeout(() => {
|
||||
if (!value || !value.trim().length) {
|
||||
setSearchedPlaces(null);
|
||||
return;
|
||||
}
|
||||
setSelectedPlace(null);
|
||||
setSearchedPlaces([]);
|
||||
setLoading(true);
|
||||
if (!value) {
|
||||
setSearchedPlaces([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
return searchPlaces(value)
|
||||
.then(setSearchedPlaces)
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleSelect = (place: IPlace) => {
|
||||
if (selectedPlace && selectedPlace.name === place.name) {
|
||||
setSelectedPlace(null);
|
||||
} else {
|
||||
setSelectedPlace(place);
|
||||
debugger;
|
||||
onLocationChange(place);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedPlace) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
onSubmit(selectedPlace)
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Location updated",
|
||||
description: "Location updated successfully",
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onOpenChange(false);
|
||||
setSelectedPlace(null);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Error updating location",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 px-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Search Location</Label>
|
||||
<Input
|
||||
placeholder="Search location"
|
||||
onChange={(e) => {
|
||||
handleSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{loading && <Loader />}
|
||||
{!loading && searchedPlaces && searchedPlaces.length === 0 && (
|
||||
<ErrorBlock description="No results found" />
|
||||
)}
|
||||
{!loading && searchedPlaces && searchedPlaces.length > 0 && (
|
||||
<div className="flex flex-col gap-2 max-h-[400px] overflow-y-auto">
|
||||
{searchedPlaces.map((place) => (
|
||||
<div
|
||||
key={place.name}
|
||||
onClick={() => handleSelect(place)}
|
||||
className={cn(
|
||||
"hover:bg-gray-300 flex justify-between items-center px-2 py-1 rounded-lg cursor-pointer",
|
||||
{
|
||||
"bg-gray-300":
|
||||
selectedPlace && selectedPlace.name === place.name,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p>{place.name}</p>
|
||||
<span className="text-xs text-gray-600">
|
||||
{place.latitude}, {place.longitude}
|
||||
</span>
|
||||
</div>
|
||||
{selectedPlace && selectedPlace.name === place.name && (
|
||||
<Check className="text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedPlace || submitting}
|
||||
>
|
||||
Add New Location
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
|
||||
import ErrorBlock from "@/components/shared/ErrorBlock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-6xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user