feat: added filters in geo heatmap
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { listAlbums } from '@/handlers/api/album.handler';
|
||||
import { IAlbum } from '@/types/album';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface IAlbumDropdownProps {
|
||||
albumIds?: string[];
|
||||
onChange: (albumIds: string[]) => void;
|
||||
}
|
||||
export default function AlbumDropdown({ albumIds, onChange }: IAlbumDropdownProps) {
|
||||
const [albums, setAlbums] = useState<IAlbum[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedAlbumIds, setSelectedAlbumIds] = useState(albumIds);
|
||||
|
||||
const fetchAlbums = async () => {
|
||||
setLoading(true);
|
||||
return listAlbums({
|
||||
sortBy: "createdAt",
|
||||
sortOrder: "desc",
|
||||
}).then((albums) => setAlbums(albums)).catch((error) => setError(error)).finally(() => setLoading(false)) ;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlbums();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (albumIds) {
|
||||
setSelectedAlbumIds(albumIds);
|
||||
} else {
|
||||
setSelectedAlbumIds([]);
|
||||
}
|
||||
}, [albumIds]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
setSelectedAlbumIds([value])
|
||||
onChange([value])
|
||||
} else {
|
||||
setSelectedAlbumIds([])
|
||||
onChange([])
|
||||
}
|
||||
}}
|
||||
value={selectedAlbumIds?.[0] ?? ""}
|
||||
>
|
||||
<SelectTrigger className='!p-2'>
|
||||
<SelectValue placeholder={'Select album'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{albums.map((album) => (
|
||||
<SelectItem key={album.id} value={album.id}>{album.albumName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { IPerson } from '@/types/person';
|
||||
import { IPersonListFilters, listPeople } from '@/handlers/api/people.handler';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
interface IPeopleDropdownProps {
|
||||
peopleIds?: string[];
|
||||
onChange: (peopleIds: string[]) => void;
|
||||
}
|
||||
export default function PeopleDropdown({ peopleIds, onChange }: IPeopleDropdownProps) {
|
||||
const [people, setPeople] = useState<IPerson[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedPeopleIds, setSelectedPeopleIds] = useState(peopleIds);
|
||||
const [filters, setFilters] = useState<IPersonListFilters>({
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sort: "createdAt",
|
||||
sortOrder: "desc",
|
||||
visibility: "visible",
|
||||
type: "named",
|
||||
});
|
||||
|
||||
const fetchPeople = async () => {
|
||||
setLoading(true);
|
||||
return listPeople(filters).then((people) => setPeople(people.people)).catch((error) => setError(error)).finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPeople();
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (peopleIds) {
|
||||
setSelectedPeopleIds(peopleIds);
|
||||
} else {
|
||||
setSelectedPeopleIds([]);
|
||||
}
|
||||
}, [peopleIds]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select onValueChange={(value) => {
|
||||
if (value) {
|
||||
setSelectedPeopleIds([value])
|
||||
onChange([value])
|
||||
} else {
|
||||
setSelectedPeopleIds([])
|
||||
onChange([])
|
||||
}
|
||||
}} value={selectedPeopleIds?.[0] ?? ""}>
|
||||
<SelectTrigger className='!p-2'>
|
||||
<SelectValue placeholder={'Select people'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent >
|
||||
{people.map((person) => (
|
||||
<SelectItem key={person.id} value={person.id}>{person.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -26,21 +26,27 @@ export interface IComboBoxProps {
|
||||
onSelect?: (value: string) => void
|
||||
onOpenChange?: (open: boolean) => void
|
||||
closeOnSelect?: boolean
|
||||
label?: string
|
||||
onTextChange?: (text: string) => void
|
||||
}
|
||||
|
||||
export function Combobox(
|
||||
{ options, onSelect, onOpenChange, closeOnSelect }: IComboBoxProps
|
||||
{ options, onSelect, onOpenChange, closeOnSelect, label, onTextChange }: IComboBoxProps
|
||||
) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={(openState) => {
|
||||
setOpen(openState)
|
||||
onOpenChange?.(openState)
|
||||
}}>
|
||||
<PopoverTrigger asChild>
|
||||
<Filter />
|
||||
<div className="flex items-center text-sm gap-2 cursor-pointer">
|
||||
<Filter size={16} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
|
||||
@@ -61,8 +61,12 @@ export const findAssets = async (query: string) => {
|
||||
return API.post(FIND_ASSETS, { query });
|
||||
}
|
||||
|
||||
export const getAssetGeoHeatmap = async () => {
|
||||
return API.get(ASSET_GEO_HEATMAP_PATH);
|
||||
export interface IHeatMapParams {
|
||||
albumIds?: string;
|
||||
peopleIds?: string;
|
||||
}
|
||||
export const getAssetGeoHeatmap = async (filters: IHeatMapParams) => {
|
||||
return API.get(ASSET_GEO_HEATMAP_PATH, filters);
|
||||
}
|
||||
|
||||
export const deleteAssets = async (ids: string[]) => {
|
||||
|
||||
@@ -3,26 +3,34 @@ import { NextApiResponse } from "next";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { NextApiRequest } from "next";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import { assetFaces, assets, exif, person } from "@/schema";
|
||||
import { and, eq, inArray, isNotNull } from "drizzle-orm";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const currentUser = await getCurrentUser(req);
|
||||
const { albumIds, peopleIds } = req.query as { albumIds: string, peopleIds: string };
|
||||
if (!currentUser) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
|
||||
const dbAssets = await db.select({
|
||||
assetId: assets.id,
|
||||
latitude: exif.latitude,
|
||||
longitude: exif.longitude,
|
||||
}).from(assets)
|
||||
.innerJoin(exif, eq(assets.id, exif.assetId))
|
||||
.innerJoin(albumsAssetsAssets, eq(assets.id, albumsAssetsAssets.assetsId))
|
||||
.innerJoin(assetFaces, eq(albumsAssetsAssets.assetsId, assetFaces.assetId))
|
||||
.innerJoin(person, eq(assetFaces.personId, person.id))
|
||||
.where(
|
||||
and(
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
isNotNull(exif.latitude),
|
||||
isNotNull(exif.longitude)
|
||||
isNotNull(exif.longitude),
|
||||
albumIds?.length > 0 ? inArray(albumsAssetsAssets.albumsId, [albumIds]) : undefined,
|
||||
peopleIds?.length > 0 ? inArray(person.id, [peopleIds]) : undefined
|
||||
)
|
||||
);
|
||||
const heatmapData = dbAssets.map((asset) => [
|
||||
|
||||
@@ -4,13 +4,19 @@ import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useMap, useMapsLibrary } from '@vis.gl/react-google-maps';
|
||||
import { APIProvider, Map } from '@vis.gl/react-google-maps';
|
||||
import PageLayout from '@/components/layouts/PageLayout';
|
||||
import { getAssetGeoHeatmap } from '@/handlers/api/asset.handler';
|
||||
import { getAssetGeoHeatmap, IHeatMapParams } from '@/handlers/api/asset.handler';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { MapPinX } from 'lucide-react';
|
||||
import { MapPinX, X } from 'lucide-react';
|
||||
import AlbumDropdown from '@/components/shared/AlbumDropdown';
|
||||
import PeopleDropdown from '@/components/shared/PeopleDropdown';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface IHeatMapProps {
|
||||
filters: IHeatMapParams;
|
||||
}
|
||||
|
||||
const HeatMap = ({ filters }: IHeatMapProps) => {
|
||||
|
||||
const HeatMap = () => {
|
||||
|
||||
const map = useMap();
|
||||
const visualization = useMapsLibrary('visualization');
|
||||
|
||||
@@ -27,14 +33,15 @@ const HeatMap = () => {
|
||||
useEffect(() => {
|
||||
if (!heatmap) return;
|
||||
|
||||
getAssetGeoHeatmap().then((data) => {
|
||||
getAssetGeoHeatmap(filters).then((data) => {
|
||||
heatmap.setData([]);
|
||||
heatmap.setData(data.map(([lng, lat]: [number, number]) => ({
|
||||
location: new google.maps.LatLng(lat, lng),
|
||||
weight: 10
|
||||
})));
|
||||
});
|
||||
|
||||
}, [heatmap]);
|
||||
}, [heatmap, filters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!heatmap) return;
|
||||
@@ -53,6 +60,10 @@ export default function GeoHeatmap() {
|
||||
const { googleMapsApiKey } = useConfig();
|
||||
const { theme } = useTheme();
|
||||
const [mapId, setMapId] = useState('7a9e2ebecd32a903');
|
||||
const [filters, setFilters] = useState<{
|
||||
albumIds?: string
|
||||
peopleIds?: string
|
||||
}>({ });
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'light') {
|
||||
@@ -65,21 +76,42 @@ export default function GeoHeatmap() {
|
||||
|
||||
return (
|
||||
<PageLayout className="!p-0 !mb-0">
|
||||
<Header leftComponent="Geo Heatmap" />
|
||||
<Header leftComponent="Geo Heatmap" rightComponent={
|
||||
<>
|
||||
<AlbumDropdown albumIds={filters.albumIds ? [filters.albumIds] : []} onChange={(albumIds) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
albumIds: albumIds?.[0]
|
||||
})
|
||||
}} />
|
||||
<PeopleDropdown peopleIds={filters.peopleIds ? [filters.peopleIds] : []} onChange={(peopleIds) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
peopleIds: peopleIds?.[0]
|
||||
})
|
||||
}} />
|
||||
<Button
|
||||
variant="default" size="sm"
|
||||
disabled={Object.keys(filters).length === 0}
|
||||
onClick={() => setFilters({})}>
|
||||
<X size={16} /> Clear
|
||||
</Button>
|
||||
</>
|
||||
} />
|
||||
<div className='h-full w-full'>
|
||||
{googleMapsApiKey ? (
|
||||
<APIProvider apiKey={googleMapsApiKey}>
|
||||
<Map
|
||||
defaultCenter={{ lat: 0, lng: 0 }}
|
||||
mapId={mapId}
|
||||
defaultZoom={2}
|
||||
gestureHandling={'greedy'}
|
||||
disableDefaultUI={true}
|
||||
className='h-full w-full'
|
||||
/>
|
||||
<Map
|
||||
defaultCenter={{ lat: 0, lng: 0 }}
|
||||
mapId={mapId}
|
||||
defaultZoom={2}
|
||||
gestureHandling={'greedy'}
|
||||
disableDefaultUI={true}
|
||||
className='h-full w-full'
|
||||
/>
|
||||
|
||||
<HeatMap />
|
||||
</APIProvider>
|
||||
<HeatMap filters={filters} />
|
||||
</APIProvider>
|
||||
) : (
|
||||
<div className='h-full w-full flex items-center justify-center flex-col gap-2'>
|
||||
<MapPinX className='w-10 h-10 text-muted-foreground' />
|
||||
|
||||
Reference in New Issue
Block a user