feat: people selection in share link
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
@@ -195,7 +195,6 @@ export default function AlbumPeople({ album, onSelect, readOnly }: AlbumPeoplePr
|
||||
/>
|
||||
)}
|
||||
<LazyImage
|
||||
|
||||
role="button"
|
||||
className={
|
||||
cn("cursor-pointer h-10 w-10 min-w-10 rounded-full border-2",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { shareAlbums } from '@/handlers/api/album.handler';
|
||||
import { generateShareLink } from '@/handlers/api/shareLink.handler';
|
||||
import { IAlbum } from '@/types/album';
|
||||
import React, { ForwardedRef, forwardRef, useImperativeHandle, useState } from 'react'
|
||||
import ShareAssetsTrigger from '@/components/shared/ShareAssetsTrigger';
|
||||
|
||||
export interface IAlbumShareDialogProps {
|
||||
|
||||
@@ -169,15 +170,7 @@ const AlbumShareDialog = forwardRef(({ }: IAlbumShareDialogProps, ref: Forwarded
|
||||
Generate For {selectedAlbums.length} albums
|
||||
</Button>
|
||||
</AlertDialog>
|
||||
<AlertDialog
|
||||
title="Generate a single share link"
|
||||
description="Immich Power Tools will work like a proxy for all your albums using single share link."
|
||||
onConfirm={handleGenerateGlobalShareLink}
|
||||
>
|
||||
<Button disabled={generating}>
|
||||
Generate 1 Share Link
|
||||
</Button>
|
||||
</AlertDialog>
|
||||
<ShareAssetsTrigger filters={{ albumIds: selectedAlbums.map((album) => album.id) }} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -38,8 +38,6 @@ export default function AssetGrid({ assets, isInternal = true, selectable = fals
|
||||
}));
|
||||
}, [assets]);
|
||||
|
||||
console.log(slides);
|
||||
|
||||
const images = useMemo(() => {
|
||||
return assets.map((p) => ({
|
||||
...p,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { IPerson } from '@/types/person';
|
||||
import { cn } from '@/lib/utils';
|
||||
import LazyImage from '../ui/lazy-image';
|
||||
import { PERSON_THUBNAIL_PATH } from '@/config/routes';
|
||||
|
||||
interface PeopleListProps {
|
||||
people: IPerson[];
|
||||
onSelect: (person: IPerson) => void;
|
||||
selectedIds?: string[];
|
||||
}
|
||||
|
||||
export default function PeopleList({ people, onSelect, selectedIds }: PeopleListProps) {
|
||||
|
||||
const isSelected = useCallback((person: IPerson) => {
|
||||
return selectedIds?.includes(person.id)
|
||||
}, [selectedIds])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-x-hidden gap-2">
|
||||
{people.map((person) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700 rounded-md p-1',
|
||||
isSelected(person) ? "bg-gray-300 dark:bg-gray-700" : ""
|
||||
)}
|
||||
key={person.id}
|
||||
onClick={() => onSelect(person)}
|
||||
>
|
||||
<LazyImage
|
||||
role="button"
|
||||
className={
|
||||
cn("cursor-pointer h-10 w-10 min-w-10 rounded-full border-2",
|
||||
!!person.name ? "border-green-500" : "border-gray-500",
|
||||
)
|
||||
}
|
||||
src={person.thumbnailPath} alt={person.name} />
|
||||
<div className='flex flex-col text-sm'>
|
||||
<p>{person.name || "No Name"}</p>
|
||||
<p className={
|
||||
cn("text-xs text-gray-500 dark:text-gray-400")
|
||||
}>{person.assetCount} photos</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { ShareLinkFilters } from '@/types/shareLink';
|
||||
import { generateShareLink } from '@/handlers/api/shareLink.handler';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
import { Checkbox } from '../ui/checkbox';
|
||||
import { Switch } from '../ui/switch';
|
||||
|
||||
|
||||
interface ShareAssetsTriggerProps {
|
||||
@@ -17,10 +19,11 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [config, setConfig] = useState<Partial<ShareLinkFilters>>({});
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setLoading(true);
|
||||
return generateShareLink(filters).then(({ link }) => {
|
||||
return generateShareLink({ ...filters, ...config }).then(({ link }) => {
|
||||
setGeneratedLink(link);
|
||||
}).catch((err) => {
|
||||
setErrorMessage(err.message);
|
||||
@@ -50,7 +53,7 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets
|
||||
<DialogTitle>Share Assets</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Share your assets with your friends and family.
|
||||
Generate a share link for the selected assets (either bunch of albums or bunch of people) and share it with your friends and family.
|
||||
</DialogDescription>
|
||||
{errorMessage && <div className="text-red-500">{errorMessage}</div>}
|
||||
{generatedLink ? <div className="flex flex-col gap-2">
|
||||
@@ -59,9 +62,23 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
This is a stateless link, it will not work if you leave the page. Which means it cannot be expired.
|
||||
</p>
|
||||
<Button onClick={handleCopy}>{copied ? 'Copied' : 'Copy'}</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button className="w-full" onClick={handleCopy}>{copied ? 'Copied' : 'Copy'}</Button>
|
||||
<Button className="w-full" variant="outline" onClick={handleGenerate}>Generate New Link</Button>
|
||||
</div>
|
||||
</div> : (
|
||||
<Button onClick={handleGenerate} disabled={loading}>Generate Share Link</Button>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label className="text-sm m-0">Show People</Label>
|
||||
<p className="text-xs text-muted-foreground m-0">
|
||||
Show the list of people in the shared photos
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={config.p} onCheckedChange={(checked) => setConfig({ ...config, p: !!checked })} />
|
||||
</div>
|
||||
<Button onClick={handleGenerate} disabled={loading}>Generate Share Link</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -40,7 +40,9 @@ export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/
|
||||
export const ASSET_VIDEO_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/video/" + id;
|
||||
export const ASSET_GEO_HEATMAP_PATH = BASE_API_ENDPOINT + "/assets/geo-heatmap";
|
||||
|
||||
export const ASSET_SHARE_THUMBNAIL_PATH = (id: string, size: string = "thumbnail", token: string) => BASE_PROXY_ENDPOINT + "/asset/share-thumbnail/" + id + "?size=" + size + "&token=" + token;
|
||||
export const ASSET_SHARE_THUMBNAIL_PATH = (
|
||||
{ id, size, token, isPeople }: { id: string, size: string, token: string, isPeople: boolean }
|
||||
) => BASE_PROXY_ENDPOINT + "/asset/share-thumbnail/" + id + "?size=" + size + "&token=" + token + "&p=" + isPeople;
|
||||
|
||||
// Location
|
||||
|
||||
@@ -67,3 +69,4 @@ export const FIND_ASSETS = BASE_API_ENDPOINT + "/find/search";
|
||||
export const SHARE_LINK_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token;
|
||||
export const SHARE_LINK_GENERATE_PATH = BASE_API_ENDPOINT + "/share-link/generate";
|
||||
export const SHARE_LINK_ASSETS_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/assets";
|
||||
export const SHARE_LINK_PEOPLE_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/people";
|
||||
@@ -1,6 +1,10 @@
|
||||
|
||||
import { SHARE_LINK_ASSETS_PATH, SHARE_LINK_GENERATE_PATH, SHARE_LINK_PATH } from "@/config/routes";
|
||||
import { cleanUpAssets } from "@/helpers/asset.helper";
|
||||
import {
|
||||
SHARE_LINK_ASSETS_PATH,
|
||||
SHARE_LINK_GENERATE_PATH,
|
||||
SHARE_LINK_PATH,
|
||||
SHARE_LINK_PEOPLE_PATH
|
||||
} from "@/config/routes";
|
||||
import API from "@/lib/api";
|
||||
import { ShareLinkFilters } from "@/types/shareLink";
|
||||
|
||||
@@ -8,10 +12,14 @@ export const getShareLinkInfo = async (token: string) => {
|
||||
return API.get(SHARE_LINK_PATH(token));
|
||||
}
|
||||
|
||||
export const getShareLinkAssets = async (token: string) => {
|
||||
return API.get(SHARE_LINK_ASSETS_PATH(token));
|
||||
export const getShareLinkAssets = async (token: string, filters: ShareLinkFilters) => {
|
||||
return API.get(SHARE_LINK_ASSETS_PATH(token), filters);
|
||||
}
|
||||
|
||||
export const generateShareLink = async (filters: ShareLinkFilters) => {
|
||||
return API.post(SHARE_LINK_GENERATE_PATH, filters);
|
||||
}
|
||||
|
||||
export const getShareLinkPeople = async (token: string) => {
|
||||
return API.get(SHARE_LINK_PEOPLE_PATH(token));
|
||||
}
|
||||
@@ -15,10 +15,10 @@ export const cleanUpAsset = (asset: IAsset): IAsset => {
|
||||
export const cleanUpShareAsset = (asset: IAsset, token: string): IAsset => {
|
||||
return {
|
||||
...asset,
|
||||
url: ASSET_SHARE_THUMBNAIL_PATH(asset.id, "thumbnail", token),
|
||||
downloadUrl: ASSET_SHARE_THUMBNAIL_PATH(asset.id, "original", token),
|
||||
previewUrl: ASSET_SHARE_THUMBNAIL_PATH(asset.id, "preview", token),
|
||||
videoURL: ASSET_SHARE_THUMBNAIL_PATH(asset.id, "video", token),
|
||||
url: ASSET_SHARE_THUMBNAIL_PATH({ id: asset.id, size: "thumbnail", token, isPeople: false }),
|
||||
downloadUrl: ASSET_SHARE_THUMBNAIL_PATH({ id: asset.id, size: "original", token, isPeople: false }),
|
||||
previewUrl: ASSET_SHARE_THUMBNAIL_PATH({ id: asset.id, size: "preview", token, isPeople: false }),
|
||||
videoURL: ASSET_SHARE_THUMBNAIL_PATH({ id: asset.id, size: "video", token, isPeople: false }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(405).json({ message: 'Method Not Allowed' })
|
||||
}
|
||||
|
||||
const { id, size, token } = req.query;
|
||||
const { id, size, token, p } = req.query;
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Unauthorized' })
|
||||
}
|
||||
@@ -28,10 +28,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: 'Token is invalid' })
|
||||
}
|
||||
|
||||
let targetUrl = `${ENV.IMMICH_URL}/api/assets/${id}/thumbnail?size=${size || 'thumbnail'}`;
|
||||
if (size === "original") {
|
||||
targetUrl = `${ENV.IMMICH_URL}/api/assets/${id}/original`;
|
||||
}
|
||||
const resource = p === "true" ? "people" : "assets";
|
||||
const baseURL = `${ENV.IMMICH_URL}/api/${resource}/${id}`;
|
||||
const version = size === "original" ? "original" : "thumbnail";
|
||||
let targetUrl = `${baseURL}/${version}?size=${size}`;
|
||||
|
||||
try {
|
||||
// Forward the request to the target API
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@/config/db";
|
||||
import { ENV } from "@/config/environment";
|
||||
import { desc, gte, lte } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { assetFaces, assets, exif } from "@/schema";
|
||||
import { assetFaces, assets, exif, person } from "@/schema";
|
||||
import { and } from "drizzle-orm";
|
||||
import { sign, verify } from "jsonwebtoken";
|
||||
import { NextApiResponse } from "next";
|
||||
@@ -10,19 +10,21 @@ import { NextApiResponse } from "next";
|
||||
import { NextApiRequest } from "next";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { cleanUpAsset, cleanUpAssets, cleanUpShareAsset, isFlipped } from "@/helpers/asset.helper";
|
||||
import { IAsset } from "@/types/asset";
|
||||
import { parseDate } from "@/helpers/date.helper";
|
||||
import { cleanUpShareAsset, isFlipped } from "@/helpers/asset.helper";
|
||||
import { ShareLinkFilters } from "@/types/shareLink";
|
||||
|
||||
interface ShareLinkFilters {
|
||||
personIds: string[];
|
||||
albumIds: string[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
interface IQuery extends ShareLinkFilters {
|
||||
token: string
|
||||
['personIds[]']?: string[]
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token } = req.query;
|
||||
const { token, ...rest } = req.query as any as IQuery
|
||||
|
||||
const filters = {
|
||||
personIds: rest['personIds[]'] ?? rest.personIds ?? [],
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(404).json({ message: "Token not found" });
|
||||
}
|
||||
@@ -36,8 +38,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const newPreviewToken = sign({ token: token }, ENV.JWT_SECRET, {
|
||||
expiresIn: "2m"
|
||||
});
|
||||
const { personIds, albumIds, startDate, endDate } = decoded as ShareLinkFilters;
|
||||
|
||||
const { personIds = [], albumIds = [], startDate, endDate } = decoded as ShareLinkFilters;
|
||||
let filteredPersonIds: string[] = personIds ?? [];
|
||||
const queryPersonIds = filters.personIds ? (Array.isArray(filters.personIds) ? filters.personIds : [filters.personIds]) : [];
|
||||
|
||||
|
||||
if (queryPersonIds.length > 0) {
|
||||
|
||||
if (personIds.length === 0) {
|
||||
filteredPersonIds = queryPersonIds;
|
||||
} else {
|
||||
filteredPersonIds = queryPersonIds.filter((id) => personIds.includes(id));
|
||||
}
|
||||
}
|
||||
|
||||
const dbAssets = await db.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
@@ -64,7 +79,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
.innerJoin(albums, eq(albumsAssetsAssets.albumsId, albums.id))
|
||||
.innerJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
personIds?.length > 0 ? inArray(assetFaces.personId, personIds) : undefined,
|
||||
filteredPersonIds?.length > 0 ? inArray(assetFaces.personId, filteredPersonIds) : undefined,
|
||||
albumIds?.length > 0 ? inArray(albums.id, albumIds) : undefined,
|
||||
startDate ? gte(assets.createdAt, new Date(startDate)) : undefined,
|
||||
endDate ? lte(assets.createdAt, new Date(endDate)) : undefined,
|
||||
@@ -85,5 +100,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
}).map((asset) => cleanUpShareAsset(asset, newPreviewToken as string));
|
||||
|
||||
return res.status(200).json(cleanedAssets);
|
||||
return res.status(200).json({
|
||||
assets: cleanedAssets,
|
||||
filters: decoded as ShareLinkFilters
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NextApiResponse } from "next";
|
||||
|
||||
import { ENV } from "@/config/environment";
|
||||
import { ShareLinkFilters } from "@/types/shareLink";
|
||||
import { verify } from "jsonwebtoken";
|
||||
import { NextApiRequest } from "next";
|
||||
|
||||
export default async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token } = req.query;
|
||||
if (!token) {
|
||||
return res.status(404).json({ message: "Token not found" });
|
||||
}
|
||||
|
||||
const decoded = verify(token as string, ENV.JWT_SECRET);
|
||||
|
||||
if (!decoded) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
return res.status(200).json(decoded);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { db } from "@/config/db";
|
||||
import { ENV } from "@/config/environment";
|
||||
import { count, desc, gte, isNotNull, lte, ne } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { assetFaces, assets, exif, person } from "@/schema";
|
||||
import { and } from "drizzle-orm";
|
||||
import { sign, verify } from "jsonwebtoken";
|
||||
import { NextApiResponse } from "next";
|
||||
|
||||
import { NextApiRequest } from "next";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { ASSET_SHARE_THUMBNAIL_PATH } from "@/config/routes";
|
||||
|
||||
interface ShareLinkFilters {
|
||||
personIds: string[];
|
||||
albumIds: string[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token } = req.query;
|
||||
if (!token) {
|
||||
return res.status(404).json({ message: "Token not found" });
|
||||
}
|
||||
|
||||
const decoded = verify(token as string, ENV.JWT_SECRET);
|
||||
|
||||
if (!decoded) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const newPreviewToken = sign({ token: token }, ENV.JWT_SECRET, {
|
||||
expiresIn: "2m"
|
||||
});
|
||||
const { personIds, albumIds, startDate, endDate } = decoded as ShareLinkFilters;
|
||||
|
||||
const dbPeople = await db.select({
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
thumbnailAssetId: person.faceAssetId,
|
||||
assetCount: count(assets.id),
|
||||
}).from(assets)
|
||||
.innerJoin(assetFaces, eq(assets.id, assetFaces.assetId))
|
||||
.innerJoin(albumsAssetsAssets, eq(assets.id, albumsAssetsAssets.assetsId))
|
||||
.innerJoin(person, eq(assetFaces.personId, person.id))
|
||||
.innerJoin(albums, eq(albumsAssetsAssets.albumsId, albums.id))
|
||||
.innerJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
personIds?.length > 0 ? inArray(assetFaces.personId, personIds) : undefined,
|
||||
albumIds?.length > 0 ? inArray(albums.id, albumIds) : undefined,
|
||||
startDate ? gte(assets.createdAt, new Date(startDate)) : undefined,
|
||||
endDate ? lte(assets.createdAt, new Date(endDate)) : undefined,
|
||||
eq(assets.isArchived, false),
|
||||
eq(assets.isVisible, true),
|
||||
eq(assets.isOffline, false),
|
||||
isNotNull(person.id),
|
||||
ne(person.name, "")
|
||||
))
|
||||
.groupBy(person.id)
|
||||
.orderBy(desc(count(assets.id)));
|
||||
|
||||
const cleanedPeople = dbPeople.map((person) => {
|
||||
return {
|
||||
...person,
|
||||
thumbnailPath: person.thumbnailAssetId ? ASSET_SHARE_THUMBNAIL_PATH({ id: person.id, size: "thumbnail", token: newPreviewToken, isPeople: true }) : null
|
||||
}
|
||||
})
|
||||
|
||||
return res.status(200).json(cleanedPeople);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const currentUser = await getCurrentUser(req);
|
||||
const filters = req.body;
|
||||
|
||||
console.log(filters);
|
||||
if (!currentUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
@@ -18,7 +19,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
console.log(filters);
|
||||
const token = sign(filters, ENV.JWT_SECRET);
|
||||
return res.status(200).json({ link: `${ENV.POWER_TOOLS_ENDPOINT_URL}/s/${token}` });
|
||||
}
|
||||
@@ -28,7 +28,6 @@ const HeatMap = () => {
|
||||
if (!heatmap) return;
|
||||
|
||||
getAssetGeoHeatmap().then((data) => {
|
||||
console.log(data);
|
||||
heatmap.setData(data.map(([lng, lat]: [number, number]) => ({
|
||||
location: new google.maps.LatLng(lat, lng),
|
||||
weight: 10
|
||||
|
||||
+100
-23
@@ -1,30 +1,61 @@
|
||||
import PageLayout from '@/components/layouts/PageLayout'
|
||||
import Header from '@/components/shared/Header'
|
||||
import Loader from '@/components/ui/loader'
|
||||
import { useConfig } from '@/contexts/ConfigContext'
|
||||
import { IAlbum } from '@/types/album'
|
||||
import Link from 'next/link'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import AlbumPeople from '@/components/albums/info/AlbumPeople'
|
||||
import AlbumImages from '@/components/albums/info/AlbumImages'
|
||||
import { Camera, ExternalLink, Users } from 'lucide-react'
|
||||
import { Camera } from 'lucide-react'
|
||||
import { humanizeNumber } from '@/helpers/string.helper'
|
||||
import { getShareLinkInfo } from '@/handlers/api/shareLink.handler'
|
||||
import { getShareLinkAssets, getShareLinkInfo, getShareLinkPeople } from '@/handlers/api/shareLink.handler'
|
||||
import AssetGrid from '@/components/shared/AssetGrid'
|
||||
import { IAsset } from '@/types/asset'
|
||||
import { IPerson } from '@/types/person'
|
||||
import PeopleList from '@/components/shared/PeopleList'
|
||||
import { ShareLinkFilters } from '@/types/shareLink'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function AlbumListPage() {
|
||||
const router = useRouter()
|
||||
const { token } = router.query as { token: string }
|
||||
const { pathname, query } = router
|
||||
const { token, personIds } = query as { token: string, personIds: string[] }
|
||||
const [assets, setAssets] = useState<IAsset[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [people, setPeople] = useState<IPerson[]>([])
|
||||
const [peopleLoading, setPeopleLoading] = useState(false)
|
||||
const [config, setConfig] = useState<ShareLinkFilters>({})
|
||||
const [filters, setFilters] = useState<ShareLinkFilters | null>(null)
|
||||
|
||||
const fetchAlbumInfo = async () => {
|
||||
|
||||
const handleSelectPerson = (person: IPerson) => {
|
||||
const personIds = query.personIds as string[]
|
||||
let currentPersonIds = personIds
|
||||
if (!personIds) {
|
||||
currentPersonIds = []
|
||||
} else {
|
||||
currentPersonIds = Array.isArray(personIds) ? personIds : [personIds]
|
||||
}
|
||||
const isPersonSelected = currentPersonIds.includes(person.id);
|
||||
const newPersonIds = isPersonSelected ? currentPersonIds.filter((id) => id !== person.id) : [...currentPersonIds, person.id];
|
||||
|
||||
router.push({
|
||||
pathname: pathname,
|
||||
query: {
|
||||
...query,
|
||||
personIds: newPersonIds
|
||||
}
|
||||
})
|
||||
setFilters({
|
||||
personIds: newPersonIds
|
||||
})
|
||||
}
|
||||
|
||||
const fetchAssets = async () => {
|
||||
if (!filters) return
|
||||
setLoading(true)
|
||||
getShareLinkInfo(token)
|
||||
.then(setAssets)
|
||||
getShareLinkAssets(token, filters)
|
||||
.then(({ assets }) => {
|
||||
setAssets(assets)
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.message)
|
||||
})
|
||||
@@ -33,23 +64,69 @@ export default function AlbumListPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const fetchAlbumInfo = async () => {
|
||||
setLoading(true)
|
||||
getShareLinkInfo(token)
|
||||
.then((conf) => {
|
||||
setConfig(conf)
|
||||
if (conf.p) {
|
||||
fetchPeople()
|
||||
}
|
||||
setFilters({
|
||||
personIds: query.personIds as string[]
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.message)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPeople = async () => {
|
||||
setPeopleLoading(true)
|
||||
getShareLinkPeople(token)
|
||||
.then(setPeople)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.message)
|
||||
})
|
||||
.finally(() => {
|
||||
setPeopleLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlbumInfo()
|
||||
}, [])
|
||||
|
||||
const renderContent = () => {
|
||||
useEffect(() => {
|
||||
if (!filters) return
|
||||
fetchAssets()
|
||||
}, [filters])
|
||||
|
||||
if (loading) {
|
||||
return <Loader />
|
||||
}
|
||||
else if (errorMessage) {
|
||||
return <div>{errorMessage}</div>
|
||||
}
|
||||
const renderContent = () => {
|
||||
if (loading) return <Loader />
|
||||
else if (errorMessage) return <div>{errorMessage}</div>
|
||||
return (
|
||||
<AssetGrid
|
||||
assets={assets}
|
||||
isInternal={false}
|
||||
/>
|
||||
<div className="flex gap-1 max-h-full">
|
||||
{config.p && (
|
||||
<div className="w-1/6 overflow-y-auto sticky top-0">
|
||||
{peopleLoading ? <Loader /> : <PeopleList
|
||||
people={people}
|
||||
onSelect={handleSelectPerson}
|
||||
selectedIds={query.personIds as string[]}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx("overflow-y-auto sticky top-0", config.p ? "w-5/6" : "w-full")}>
|
||||
{loading ? <Loader /> : <AssetGrid
|
||||
assets={assets}
|
||||
isInternal={false}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
|
||||
Vendored
+1
@@ -3,4 +3,5 @@ export interface ShareLinkFilters {
|
||||
albumIds?: string[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
p?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user