feat: added map location tagging feature

Signed-off-by: Varun Raj <mailme@varunraj.in>
This commit is contained in:
Varun Raj committed 2024-08-30 09:13:47 +05:30
1 parent 86ae90b16a
commit d35c1ca1dd
8 files changed
+404 -168

No files matched your search

BIN
View File
Binary file not shown.
+1
View File
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
)
}
@@ -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>
)
}
+53
View File
@@ -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 }
+1 -1
View File
@@ -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";