diff --git a/src/components/albums/list/AlbumThumbnail.tsx b/src/components/albums/list/AlbumThumbnail.tsx index a4fb7a1..2278ccd 100644 --- a/src/components/albums/list/AlbumThumbnail.tsx +++ b/src/components/albums/list/AlbumThumbnail.tsx @@ -1,23 +1,23 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import Link from 'next/link'; -import { useConfig } from '@/contexts/ConfigContext'; import { humanizeBytes, humanizeNumber, pluralize } from '@/helpers/string.helper'; import LazyImage from '@/components/ui/lazy-image'; import { ASSET_THUMBNAIL_PATH } from '@/config/routes'; import { IAlbum } from '@/types/album'; -import { Badge } from '@/components/ui/badge'; import { formatDate } from '@/helpers/date.helper'; import { Checkbox } from '@/components/ui/checkbox'; import { differenceInDays } from 'date-fns'; -import { FaceIcon } from '@radix-ui/react-icons'; -import { Calendar, Camera, Image, User } from 'lucide-react'; +import { Calendar, Camera } from 'lucide-react'; interface IAlbumThumbnailProps { album: IAlbum; onSelect: (checked: boolean) => void; + selected: boolean; } -export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps) { +export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThumbnailProps) { + const [isSelected, setIsSelected] = useState(selected); + const numberOfDays = useMemo(() => { return differenceInDays(album.lastPhotoDate, album.firstPhotoDate); }, [album.firstPhotoDate, album.lastPhotoDate]); @@ -39,6 +39,7 @@ export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps {formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy')} - {formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy')} diff --git a/src/components/albums/share/AlbumShareDialog.tsx b/src/components/albums/share/AlbumShareDialog.tsx new file mode 100644 index 0000000..e82ee73 --- /dev/null +++ b/src/components/albums/share/AlbumShareDialog.tsx @@ -0,0 +1,137 @@ +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Dialog, DialogTitle, DialogHeader, DialogContent } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { shareAlbums } from '@/handlers/api/album.handler'; +import { IAlbum } from '@/types/album'; +import React, { ForwardedRef, forwardRef, useImperativeHandle, useState } from 'react' + +export interface IAlbumShareDialogProps { + +} + +export interface IAlbumShareDialogRef { + open: (selectedAlbums: IAlbum[]) => void; + close: () => void; +} + +interface IAlbumWithLink extends IAlbum { + shareLink?: string; + allowDownload?: boolean; + allowUpload?: boolean; + showMetadata?: boolean; +} + +const AlbumShareDialog = forwardRef(({ }: IAlbumShareDialogProps, ref: ForwardedRef) => { + const [open, setOpen] = useState(false); + const [selectedAlbums, setSelectedAlbums] = useState([]); + const [generating, setGenerating] = useState(false); + const [error, setError] = useState(null); + const [generated, setGenerated] = useState(false); + + const handleGenerateShareLink = async () => { + setGenerating(true); + const data = selectedAlbums.map((album) => ({ + albumId: album.id, + albumName: album.albumName, + assetCount: album.assetCount, + allowDownload: !!album.allowDownload, + allowUpload: !!album.allowUpload, + showMetadata: !!album.showMetadata, + })); + + return shareAlbums(data).then((updatedAlbums) => { + setSelectedAlbums(updatedAlbums); + setGenerated(true); + }) + .catch((error) => { + setError(error.message); + }) + .finally(() => { + setGenerating(false); + }); + } + + const handleAllowPropertyChange = (albumId: string, property: string, checked: boolean) => { + setSelectedAlbums((prevAlbums) => prevAlbums.map((album) => album.id === albumId ? { ...album, [property]: checked } : album)); + } + + + useImperativeHandle(ref, () => ({ + open: (selectedAlbums: IAlbum[]) => { + setSelectedAlbums(selectedAlbums.map((album) => ({ ...album, allowDownload: true, allowUpload: true, showMetadata: true }))); + setOpen(true); + }, + close: () => { + setOpen(false); + } + })); + + return ( + + + + Share {selectedAlbums.length} albums + +
+ {error &&
{error}
} +
    + {selectedAlbums.map((album) => ( +
  1. +
    +

    {album.albumName}

    +

    {album.assetCount} Items

    +
    +
    + {album.shareLink ? +

    {album.shareLink}

    : ( + <> +

    No share link generated

    +
    +
    + handleAllowPropertyChange(album.id, 'allowDownload', !!checked)} /> + +
    +
    + handleAllowPropertyChange(album.id, 'allowUpload', !!checked)} /> + +
    +
    + handleAllowPropertyChange(album.id, 'showMetadata', !!checked)} /> + +
    + +
    + + )} +
    +
  2. + + ))} +
+ {generated ? ( + <> +

Share links all generated

+ + ) : ( + <> + {generating ?
+

Generating share links...

+
:
+ + +
} + + )} + +
+
+
+ ) +}) + +AlbumShareDialog.displayName = "AlbumShareDialog"; + +export default AlbumShareDialog; diff --git a/src/config/routes.ts b/src/config/routes.ts index a57bd36..4afe671 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -25,6 +25,7 @@ export const ALBUM_PEOPLE_PATH = (id: string) => BASE_API_ENDPOINT + "/albums/" export const ALBUM_ASSETS_PATH = (id: string) => BASE_API_ENDPOINT + "/albums/" + id + "/assets"; export const CREATE_ALBUM_PATH = BASE_PROXY_ENDPOINT + "/albums"; export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets"; +export const SHARE_ALBUMS_PATH = BASE_API_ENDPOINT + "/albums/share"; // Assets export const LIST_MISSING_LOCATION_DATES_PATH = BASE_API_ENDPOINT + "/assets/missing-location-dates"; diff --git a/src/handlers/api/album.handler.ts b/src/handlers/api/album.handler.ts index 2e39f7e..4554601 100644 --- a/src/handlers/api/album.handler.ts +++ b/src/handlers/api/album.handler.ts @@ -1,4 +1,4 @@ -import { ADD_ASSETS_ALBUMS_PATH, ALBUM_ASSETS_PATH, ALBUM_INFO_PATH, ALBUM_PEOPLE_PATH, CREATE_ALBUM_PATH, LIST_ALBUMS_PATH, LIST_POTENTIAL_ALBUMS_ASSETS_PATH, LIST_POTENTIAL_ALBUMS_DATES_PATH } from "@/config/routes"; +import { ADD_ASSETS_ALBUMS_PATH, ALBUM_ASSETS_PATH, ALBUM_INFO_PATH, ALBUM_PEOPLE_PATH, CREATE_ALBUM_PATH, LIST_ALBUMS_PATH, LIST_POTENTIAL_ALBUMS_ASSETS_PATH, LIST_POTENTIAL_ALBUMS_DATES_PATH, SHARE_ALBUMS_PATH } from "@/config/routes"; import { cleanUpAsset } from "@/helpers/asset.helper"; import API from "@/lib/api"; import { IAlbumCreate } from "@/types/album"; @@ -45,3 +45,7 @@ export const addAssetToAlbum = async (albumId: string, assetIds: string[]) => { export const createAlbum = async (albumData: IAlbumCreate) => { return API.post(CREATE_ALBUM_PATH, albumData); } + +export const shareAlbums = async (albums: { albumId: string, allowDownload: boolean, allowUpload: boolean, showMetadata: boolean }[]) => { + return API.post(SHARE_ALBUMS_PATH, { albums }); +} \ No newline at end of file diff --git a/src/pages/albums/[albumId].tsx b/src/pages/albums/[albumId].tsx index d529562..3b40c0d 100644 --- a/src/pages/albums/[albumId].tsx +++ b/src/pages/albums/[albumId].tsx @@ -1,20 +1,16 @@ -import { ASSET_THUMBNAIL_PATH } from '@/config/routes' import PageLayout from '@/components/layouts/PageLayout' import Header from '@/components/shared/Header' import Loader from '@/components/ui/loader' import { useConfig } from '@/contexts/ConfigContext' -import { getAlbumInfo, listAlbums } from '@/handlers/api/album.handler' +import { getAlbumInfo } from '@/handlers/api/album.handler' import { IAlbum } from '@/types/album' -import Image from 'next/image' import Link from 'next/link' import React, { useEffect, useState } from 'react' -import AlbumThumbnail from '@/components/albums/list/AlbumThumbnail' -import { Button } from '@/components/ui/button' import { useRouter } from 'next/router' import AlbumPeople from '@/components/albums/info/AlbumPeople' import AlbumImages from '@/components/albums/info/AlbumImages' -import { Camera, ExternalLink, LinkIcon, Users } from 'lucide-react' -import { humanizeNumber, pluralize } from '@/helpers/string.helper' +import { Camera, ExternalLink, Users } from 'lucide-react' +import { humanizeNumber } from '@/helpers/string.helper' export default function AlbumListPage() { const { exImmichUrl } = useConfig() diff --git a/src/pages/albums/index.tsx b/src/pages/albums/index.tsx index 05857d7..182d2ff 100644 --- a/src/pages/albums/index.tsx +++ b/src/pages/albums/index.tsx @@ -1,20 +1,17 @@ -import { ASSET_THUMBNAIL_PATH } from '@/config/routes' import PageLayout from '@/components/layouts/PageLayout' import Header from '@/components/shared/Header' import Loader from '@/components/ui/loader' import { useConfig } from '@/contexts/ConfigContext' import { listAlbums } from '@/handlers/api/album.handler' import { IAlbum } from '@/types/album' -import Image from 'next/image' -import Link from 'next/link' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import AlbumThumbnail from '@/components/albums/list/AlbumThumbnail' import { Button } from '@/components/ui/button' import { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select' import { useRouter } from 'next/router' -import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons' -import { SortAsc, SortDesc } from 'lucide-react' +import { Share, SortAsc, SortDesc } from 'lucide-react' import { Input } from '@/components/ui/input' +import AlbumShareDialog, { IAlbumShareDialogRef } from '@/components/albums/share/AlbumShareDialog' const SORT_BY_OPTIONS = [ { value: 'lastPhotoDate', label: 'Last Photo Date' }, @@ -27,7 +24,6 @@ const SORT_BY_OPTIONS = [ { value: 'faceCount', label: 'Number of People' }, ] export default function AlbumListPage() { - const { exImmichUrl } = useConfig() const router = useRouter() const [search, setSearch] = useState('') const { query, pathname } = router @@ -35,7 +31,8 @@ export default function AlbumListPage() { const [albums, setAlbums] = useState([]) const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') - const [selectedAlbums, setSelectedAlbums] = useState([]) + const [selectedAlbumsIds, setSelectedAlbumsIds] = useState([]) + const albumShareDialogRef = useRef(null); const selectedSortBy = useMemo(() => SORT_BY_OPTIONS.find((option) => option.value === sortBy), [sortBy]) @@ -55,15 +52,18 @@ export default function AlbumListPage() { }) } + const selectedAlbums = useMemo(() => albums.filter((album) => selectedAlbumsIds.includes(album.id)), [albums, selectedAlbumsIds]) + useEffect(() => { fetchAlbums() }, [sortBy, sortOrder]) + const handleSelect = (checked: boolean, albumId: string) => { if (checked) { - setSelectedAlbums([...selectedAlbums, albumId]) + setSelectedAlbumsIds([...selectedAlbumsIds, albumId]) } else { - setSelectedAlbums(selectedAlbums.filter((id) => id !== albumId)) + setSelectedAlbumsIds(selectedAlbumsIds.filter((id) => id !== albumId)) } } @@ -78,7 +78,12 @@ export default function AlbumListPage() { return (
{searchedAlbums.map((album) => ( - handleSelect(checked, album.id)} /> + handleSelect(checked, album.id)} + /> ))}
) @@ -89,6 +94,16 @@ export default function AlbumListPage() { leftComponent="Manage Albums" rightComponent={
+ {!!selectedAlbumsIds.length && ( + + )} setSearch(e.target.value)} />