feat: added map location tagging feature
Signed-off-by: Varun Raj <mailme@varunraj.in>
This commit is contained in:
1 parent
86ae90b16a
commit
d35c1ca1dd
8 files changed
+404
-168
No files matched your search
@@ -24,6 +24,7 @@
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import ErrorBlock from "@/components/shared/ErrorBlock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
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 { 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, { useRef, useState } from "react";
|
||||
|
||||
interface ITagMissingLocationDialogProps {
|
||||
onSubmit: (place: IPlace) => Promise<any>;
|
||||
}
|
||||
export default function TagMissingLocationDialog({
|
||||
onSubmit,
|
||||
}: ITagMissingLocationDialogProps) {
|
||||
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 [open, setOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selectedPlace) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
onSubmit(selectedPlace)
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Location updated",
|
||||
description: "Location updated successfully",
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
setOpen(false);
|
||||
setSelectedPlace(null);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Error updating location",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Tag Missing Location</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tag Missing Location</DialogTitle>
|
||||
<DialogDescription>
|
||||
Tagging a location will add the location to the selected assets.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<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-zinc-900 flex justify-between items-center px-2 py-1 rounded-lg cursor-pointer",
|
||||
{
|
||||
"bg-zinc-900":
|
||||
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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { IPlace } from "@/types/common";
|
||||
import React, { useState } from "react";
|
||||
import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd";
|
||||
import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong";
|
||||
import { MapPinCheck } from "lucide-react";
|
||||
|
||||
interface ITagMissingLocationDialogProps {
|
||||
onSubmit: (place: IPlace) => Promise<any>;
|
||||
}
|
||||
export default function TagMissingLocationDialog({
|
||||
onSubmit,
|
||||
}: ITagMissingLocationDialogProps) {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Tag Missing Location</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tag Missing Location</DialogTitle>
|
||||
<DialogDescription>
|
||||
Tagging a location will add the location to the selected assets.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="search" className="border rounded-lg">
|
||||
<TabsList className="flex justify-between">
|
||||
<TabsTrigger value="search">Search and Pick</TabsTrigger>
|
||||
<TabsTrigger value="latlong">
|
||||
Latitude and Longitude
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="google">Google Maps</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="search">
|
||||
<TagMissingLocationSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} />
|
||||
</TabsContent>
|
||||
<TabsContent value="latlong">
|
||||
<TagMissingLocationSearchLatLong onSubmit={onSubmit} onOpenChange={setOpen} />
|
||||
</TabsContent>
|
||||
<TabsContent value="google">
|
||||
<div className="py-10 flex flex-col gap-6 items-center">
|
||||
<MapPinCheck size={48} className="text-gray-500"/>
|
||||
<p>Google Maps Coming Soon...</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
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 { 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, { useRef, useState } from "react";
|
||||
|
||||
interface ITagMissingLocationSearchAndAddProps {
|
||||
onSubmit: (place: IPlace) => Promise<any>;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
export default function TagMissingLocationSearchAndAdd(
|
||||
{
|
||||
onSubmit,
|
||||
onOpenChange
|
||||
}: 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 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);
|
||||
}
|
||||
};
|
||||
|
||||
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-zinc-900 flex justify-between items-center px-2 py-1 rounded-lg cursor-pointer",
|
||||
{
|
||||
"bg-zinc-900":
|
||||
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>
|
||||
)
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
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 { 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;
|
||||
}
|
||||
export default function TagMissingLocationSearchLatLong(
|
||||
{
|
||||
onSubmit,
|
||||
onOpenChange
|
||||
}: TagMissingLocationSearchLatLongProps
|
||||
) {
|
||||
const { toast } = useToast();
|
||||
const [formData, setFormData] = useState<IPlace>({
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
name: "",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (formData.latitude === 0 || formData.longitude === 0) {
|
||||
return false;
|
||||
}
|
||||
if (isNaN(formData.latitude) || isNaN(formData.longitude)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [formData]);
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
if (value.includes(",")) {
|
||||
const latLong = value.split(",");
|
||||
if (latLong.length === 2) {
|
||||
setFormData({
|
||||
...formData,
|
||||
latitude: parseFloat(latLong[0]),
|
||||
longitude: parseFloat(latLong[1]),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
onSubmit(formData)
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Location updated",
|
||||
description: "Location updated successfully",
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onOpenChange(false);
|
||||
setFormData({
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
name: "",
|
||||
});
|
||||
})
|
||||
.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 items-start gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Latitude</Label>
|
||||
<Input
|
||||
placeholder="Latitude"
|
||||
value={formData.latitude}
|
||||
onChange={(e) => {
|
||||
handleChange("latitude", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>
|
||||
Longitude
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Longitude"
|
||||
value={formData.longitude}
|
||||
onChange={(e) => {
|
||||
handleChange("longitude", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || submitting}
|
||||
>
|
||||
{submitting ? ("Adding...") : ("Add New Location")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -3,7 +3,7 @@ import PotentialAlbumsAssets from "@/components/albums/potential-albums/Potentia
|
||||
import PotentialAlbumsDates from "@/components/albums/potential-albums/PotentialAlbumsDates";
|
||||
import MissingLocationAssets from "@/components/assets/missing-location/MissingLocationAssets";
|
||||
import MissingLocationDates from "@/components/assets/missing-location/MissingLocationDates";
|
||||
import TagMissingLocationDialog from "@/components/assets/missing-location/TagMissingLocationDialog";
|
||||
import TagMissingLocationDialog from "@/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog";
|
||||
import PageLayout from "@/components/layouts/PageLayout";
|
||||
import Header from "@/components/shared/Header";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
Reference in new issue
Block a user