From 72dcb928efeb5b9da58761ceec75cae03eb79ea2 Mon Sep 17 00:00:00 2001 From: Varun Raj Date: Sat, 18 Jan 2025 14:44:04 +0530 Subject: [PATCH] feat: unified album creation and option to delete assets from potential albums Signed-off-by: Varun Raj --- src/components/albums/AlbumCreateDialog.tsx | 86 ------------ src/components/albums/AlbumSelectorDialog.tsx | 80 ++++++++++- .../assets-options/AssetOffsetDialog.tsx | 22 +-- .../assets/assets-options/AssetsOptions.tsx | 31 ----- .../MissingLocationAssets.tsx | 7 - src/components/shared/FloatingBar.tsx | 2 +- src/pages/albums/potential-albums.tsx | 129 ++++++++++++------ .../api/albums/potential-albums-assets.ts | 14 +- src/pages/assets/missing-locations.tsx | 11 +- 9 files changed, 197 insertions(+), 185 deletions(-) delete mode 100644 src/components/albums/AlbumCreateDialog.tsx delete mode 100644 src/components/assets/assets-options/AssetsOptions.tsx diff --git a/src/components/albums/AlbumCreateDialog.tsx b/src/components/albums/AlbumCreateDialog.tsx deleted file mode 100644 index cd20447..0000000 --- a/src/components/albums/AlbumCreateDialog.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; -import { Button } from "../ui/button"; -import { listAlbums } from "@/handlers/api/album.handler"; -import { IAlbum, IAlbumCreate } from "@/types/album"; -import { Input } from "../ui/input"; -import { Label } from "../ui/label"; -import { useToast } from "../ui/use-toast"; - -interface IProps { - onCreated?: (album: IAlbum) => Promise; - onSubmit?: (data: IAlbumCreate) => Promise; - assetIds?: string[]; -} -export default function AlbumCreateDialog({ onSubmit, assetIds }: IProps) { - const [open, setOpen] = useState(false); - const [loading, setLoading] = useState(false); - const [formData, setFormData] = useState({ - albumName: "", - }); - const { toast } = useToast(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!onSubmit) return; - setLoading(true); - onSubmit(formData) - .then(() => { - toast({ - title: "Album Created", - description: "Album created successfully", - }); - setFormData({ albumName: "" }); - setOpen(false); - }) - .catch((e) => { - toast({ - title: "Error", - description: "Failed to create album", - variant: "destructive", - }); - }) - .finally(() => { - setLoading(false); - }); - }; - - return ( - - - - - - - Create Album - - Create and add photos to a new album - - -
-
- - { - setFormData({ ...formData, albumName: e.target.value }); - }} - /> -
-
- -
-
-
-
- ); -} diff --git a/src/components/albums/AlbumSelectorDialog.tsx b/src/components/albums/AlbumSelectorDialog.tsx index d5dfc88..6e53b1b 100644 --- a/src/components/albums/AlbumSelectorDialog.tsx +++ b/src/components/albums/AlbumSelectorDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Dialog, DialogContent, @@ -9,18 +9,28 @@ import { } from "../ui/dialog"; import { Button } from "../ui/button"; import { listAlbums } from "@/handlers/api/album.handler"; -import { IAlbum } from "@/types/album"; +import { IAlbum, IAlbumCreate } from "@/types/album"; import { Input } from "../ui/input"; +import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import toast from "react-hot-toast"; +import { Label } from "../ui/label"; interface IProps { onSelected: (album: IAlbum) => Promise; + onCreated?: (album: IAlbum) => Promise; + onSubmit?: (data: IAlbumCreate) => Promise; } -export default function AlbumSelectorDialog({ onSelected }: IProps) { +export default function AlbumSelectorDialog({ onSelected, onCreated, onSubmit }: IProps) { const [open, setOpen] = useState(false); const [albums, setAlbums] = useState([]); const [loading, setLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [search, setSearch] = useState(""); + const { selectedIds, assets } = usePotentialAlbumContext(); + const [formData, setFormData] = useState({ + albumName: "", + }); const fetchData = () => { return listAlbums() @@ -42,11 +52,32 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) { }); } + + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + console.log(formData); + if (!onSubmit) return; + setLoading(true); + onSubmit(formData) + .then(() => { + toast.success("Album created successfully"); + setFormData({ albumName: "" }); + setOpen(false); + }) + .catch((e) => { + toast.error("Failed to create album"); + }) + .finally(() => { + setLoading(false); + }); + }, [onSubmit, formData]); + + useEffect(() => { if (open && !albums.length) fetchData(); }, [open]); - const renderContent = () => { + const renderContent = useCallback(() => { if (loading) return

Loading...

; if (errorMessage) return

{errorMessage}

; return ( @@ -73,12 +104,36 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) { ); - }; + }, [albums, filteredAlbums, handleSelect, loading, errorMessage]); + + const renderCreateContent = useCallback(() => { + return ( +
+

+ Create a new album and add the selected assets to it +

+
+ + { + setFormData({ ...formData, albumName: e.target.value }); + }} + /> +
+
+ +
+
+ ) + }, [loading, formData, handleSubmit]); return ( - + @@ -87,7 +142,18 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) { Select the albums you want to add the selected assets to - {renderContent()} + + + Albums + Create + + + {renderContent()} + + + {renderCreateContent()} + + ); diff --git a/src/components/assets/assets-options/AssetOffsetDialog.tsx b/src/components/assets/assets-options/AssetOffsetDialog.tsx index 882ca3f..9e29cf2 100644 --- a/src/components/assets/assets-options/AssetOffsetDialog.tsx +++ b/src/components/assets/assets-options/AssetOffsetDialog.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { IAsset } from '@/types/asset'; -import { Dialog, DialogTitle, DialogHeader, DialogContent } from '@/components/ui/dialog'; +import { Dialog, DialogTitle, DialogHeader, DialogContent, DialogTrigger } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -11,12 +11,12 @@ import { useToast } from '@/components/ui/use-toast'; interface IProps { assets: IAsset[]; - open: boolean; - toggleOpen: (open: boolean) => void; + onComplete: () => void; } -export default function AssetOffsetDialog({ assets: _assets, open, toggleOpen }: IProps) { +export default function AssetOffsetDialog({ assets: _assets, onComplete }: IProps) { const { toast } = useToast(); + const [open, setOpen] = useState(false); const [assets, setAssets] = useState(_assets); const [offsetData, setOffsetData] = useState<{ days: number, hours: number, minutes: number, seconds: number, years: number }>({ days: 0, @@ -43,7 +43,7 @@ export default function AssetOffsetDialog({ assets: _assets, open, toggleOpen }: }) .then(() => { setLoading(false); - toggleOpen(false); + setOpen(false); }) .catch((error) => { setLoading(false); @@ -52,7 +52,8 @@ export default function AssetOffsetDialog({ assets: _assets, open, toggleOpen }: }); await Promise.all(promises); setLoading(false); - toggleOpen(false); + setOpen(false); + onComplete(); toast({ title: 'Asset dates offset', description: 'Asset dates have been offset', @@ -65,12 +66,17 @@ export default function AssetOffsetDialog({ assets: _assets, open, toggleOpen }: }, [_assets]); return ( - + + + + Offset Asset Dates -
+
handleChange("years", parseInt(e.target.value))} /> diff --git a/src/components/assets/assets-options/AssetsOptions.tsx b/src/components/assets/assets-options/AssetsOptions.tsx deleted file mode 100644 index cb65e53..0000000 --- a/src/components/assets/assets-options/AssetsOptions.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from '@/components/ui/button' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { IAsset } from '@/types/asset'; -import { DotsVerticalIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'; -import { Clock, PlusIcon } from 'lucide-react' -import React, { useState } from 'react' -import AssetOffsetDialog from './AssetOffsetDialog'; - -interface IProps { - onAdd?: () => void; - assets: IAsset[]; -} -export default function AssetsOptions({ onAdd, assets }: IProps) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - - - setOpen(true)}> - Offset Asset Dates - - - - {open && } - - ) -} diff --git a/src/components/assets/missing-location/MissingLocationAssets.tsx b/src/components/assets/missing-location/MissingLocationAssets.tsx index c8b36b7..6f55085 100644 --- a/src/components/assets/missing-location/MissingLocationAssets.tsx +++ b/src/components/assets/missing-location/MissingLocationAssets.tsx @@ -1,22 +1,15 @@ import "yet-another-react-lightbox/styles.css"; -import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext"; -import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler"; import { IAsset } from "@/types/asset"; import React, { useEffect, useMemo, useState, MouseEvent } from "react"; import { Gallery } from "react-grid-gallery"; import Lightbox from "yet-another-react-lightbox"; import Captions from "yet-another-react-lightbox/plugins/captions"; import { - ArrowUpRight, - CalendarArrowDown, CalendarArrowUp, Hourglass, - Link, } from "lucide-react"; import { useMissingLocationContext } from "@/contexts/MissingLocationContext"; import { listMissingLocationAssets } from "@/handlers/api/asset.handler"; -import { formatDate, parseDate } from "@/helpers/date.helper"; -import { addDays } from "date-fns"; import { useConfig } from "@/contexts/ConfigContext"; import LazyGridImage from "@/components/ui/lazy-grid-image"; diff --git a/src/components/shared/FloatingBar.tsx b/src/components/shared/FloatingBar.tsx index 45c8cc9..73e3aa7 100644 --- a/src/components/shared/FloatingBar.tsx +++ b/src/components/shared/FloatingBar.tsx @@ -8,7 +8,7 @@ interface FloatingBarProps { export default function FloatingBar({ children }: FloatingBarProps) { return (
-
+
{children}
diff --git a/src/pages/albums/potential-albums.tsx b/src/pages/albums/potential-albums.tsx index 469afa5..b3276c1 100644 --- a/src/pages/albums/potential-albums.tsx +++ b/src/pages/albums/potential-albums.tsx @@ -1,10 +1,11 @@ -import AlbumCreateDialog from "@/components/albums/AlbumCreateDialog"; import AlbumSelectorDialog from "@/components/albums/AlbumSelectorDialog"; import PotentialAlbumsAssets from "@/components/albums/potential-albums/PotentialAlbumsAssets"; import PotentialAlbumsDates from "@/components/albums/potential-albums/PotentialAlbumsDates"; -import AssetsOptions from "@/components/assets/assets-options/AssetsOptions"; +import AssetOffsetDialog from "@/components/assets/assets-options/AssetOffsetDialog"; import PageLayout from "@/components/layouts/PageLayout"; +import FloatingBar from "@/components/shared/FloatingBar"; import Header from "@/components/shared/Header"; +import { AlertDialog } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; @@ -13,6 +14,7 @@ import PotentialAlbumContext, { IPotentialAlbumConfig, } from "@/contexts/PotentialAlbumContext"; import { addAssetToAlbum, createAlbum } from "@/handlers/api/album.handler"; +import { deleteAssets } from "@/handlers/api/asset.handler"; import { IAlbum, IAlbumCreate } from "@/types/album"; import { useRouter } from "next/router"; import React, { useMemo } from "react"; @@ -29,7 +31,7 @@ export default function PotentialAlbums() { assets: [], }); - const selectedAssets = useMemo(() => config.assets.filter((a) => config.selectedIds.includes(a.id)), [config.assets, config.selectedIds]) ; + const selectedAssets = useMemo(() => config.assets.filter((a) => config.selectedIds.includes(a.id)), [config.assets, config.selectedIds]); const handleSelect = (album: IAlbum) => { return addAssetToAlbum(album.id, config.selectedIds) @@ -66,50 +68,46 @@ export default function PotentialAlbums() { role: "editor", userId: id, }] + }).then(() => { + toast({ + title: "Album created", + description: "Album created successfully", + }); + setConfig({ ...config, selectedIds: [], assets: config.assets.filter(a => !config.selectedIds.includes(a.id)) }); }) } + const handleDelete = () => { + return deleteAssets(config.selectedIds) + .then(() => { + setConfig({ ...config, selectedIds: [], assets: config.assets.filter(a => !config.selectedIds.includes(a.id)) }); + }) + .then(() => { + toast({ + title: "Assets deleted", + description: "Assets deleted successfully", + }); + }) + .catch(() => { + toast({ + title: "Error", + description: "Failed to delete assets", + variant: "destructive", + }); + }); + } + + const handleOffsetComplete = () => { + setConfig({ + ...config, + selectedIds: [], + }); + } + return ( - +
- - {config.selectedIds.length} Selected - - {config.selectedIds.length && config.selectedIds.length === config.assets.length ? ( - - ) : ( - - )} - - a.id)} /> - { }} /> -
- } />
+ +
+

+ {config.selectedIds.length} Selected +

+
+ {config.selectedIds.length && config.selectedIds.length === config.assets.length ? ( + + ) : ( + + )} + + +
+ + + + +
+
+
); diff --git a/src/pages/api/albums/potential-albums-assets.ts b/src/pages/api/albums/potential-albums-assets.ts index 080fb8f..fa725b2 100644 --- a/src/pages/api/albums/potential-albums-assets.ts +++ b/src/pages/api/albums/potential-albums-assets.ts @@ -2,6 +2,7 @@ import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { isFlipped } from "@/helpers/asset.helper"; import { exif } from "@/schema"; import { count, desc, isNotNull, ne, sql } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -23,7 +24,8 @@ const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) => a."deletedAt", e."exifImageWidth", e."exifImageHeight", - e."dateTimeOriginal" + e."dateTimeOriginal", + e."orientation" FROM assets a LEFT JOIN @@ -47,7 +49,15 @@ export default async function handler( const currentUser = await getCurrentUser(req) const { startDate } = req.query as { startDate: string }; const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate, currentUser.id)); - return res.status(200).json(rows); + + const cleanedRows = rows.map((row: any) => { + return { + ...row, + exifImageWidth: isFlipped(row.orientation || 0) ? row.exifImageHeight : row.exifImageWidth, + exifImageHeight: isFlipped(row.orientation || 0) ? row.exifImageWidth : row.exifImageHeight, + }; + }); + return res.status(200).json(cleanedRows); } catch (error: any) { res.status(500).json({ error: error?.message, diff --git a/src/pages/assets/missing-locations.tsx b/src/pages/assets/missing-locations.tsx index 2da46e2..5b7d60d 100644 --- a/src/pages/assets/missing-locations.tsx +++ b/src/pages/assets/missing-locations.tsx @@ -1,4 +1,3 @@ -import AssetsOptions from "@/components/assets/assets-options/AssetsOptions"; import MissingLocationAssets from "@/components/assets/missing-location/MissingLocationAssets"; import MissingLocationDates from "@/components/assets/missing-location/MissingLocationDates"; import TagMissingLocationDialog from "@/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog"; @@ -18,6 +17,7 @@ import React, { useMemo } from "react"; import FloatingBar from "@/components/shared/FloatingBar"; import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from "@/components/ui/select"; import { AlertDialog } from "@/components/ui/alert-dialog"; +import AssetOffsetDialog from "@/components/assets/assets-options/AssetOffsetDialog"; export default function MissingLocations() { const { query, push } = useRouter(); @@ -53,6 +53,13 @@ export default function MissingLocations() { }) } + const handleOffsetComplete = () => { + setConfig({ + ...config, + selectedIds: [], + }); + } + return (
Date - { }} /> + } />