feat: added options to sort albums

Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
Varun Raj
2024-11-30 12:10:44 +05:30
parent 1290831ee3
commit 020ef8af38
7 changed files with 121 additions and 28 deletions
+18 -13
View File
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
import Link from 'next/link';
import { useConfig } from '@/contexts/ConfigContext';
import { humanizeNumber } from '@/helpers/string.helper';
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';
@@ -11,7 +11,7 @@ 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 { Camera, Image, User } from 'lucide-react';
import { Calendar, Camera, Image, User } from 'lucide-react';
interface IAlbumThumbnailProps {
album: IAlbum;
@@ -23,8 +23,8 @@ export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps
}, [album.firstPhotoDate, album.lastPhotoDate]);
return (
<div className="border rounded-lg overflow-hidden shadow-lg relative">
<label className="block relative">
<div className="border rounded-lg overflow-hidden shadow-lg relative group">
<label className="block relative ">
<LazyImage
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
alt={album.albumName}
@@ -35,7 +35,7 @@ export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps
objectFit: 'cover',
}}
/>
<div className="absolute bottom-0 w-full bg-gray-800/70 text-white text-center text-xs font-bold py-1">
<div className="absolute bottom-0 w-full bg-gray-800/70 text-white text-center text-xs font-bold py-1 group-hover:hidden">
{formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy')} - {formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy')}
</div>
<Checkbox
@@ -43,21 +43,26 @@ export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps
className="absolute hidden top-2 left-2 w-6 h-6 rounded-full border-gray-300 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
/>
</label>
<div className="flex items-center gap-2 absolute top-2 right-2 ">
<div className="bg-gray-800 flex items-center gap-1 text-white text-xs font-bold py-1 px-2 rounded">
{humanizeNumber(album.assetCount)} <Camera size={12} />
<div className="items-center gap-2 absolute top-2 right-2 group-hover:hidden flex ">
<div className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded">
<Camera size={12} />
{humanizeNumber(album.assetCount)}
</div>
<p className="bg-gray-800 flex items-center gap-1 text-white text-xs font-bold py-1 px-2 rounded">
{album.faceCount} <User size={12} />
<p className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded">
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`} <Calendar size={12} />
</p>
</div>
<div className="p-4">
<Link href={`/albums/${album.id}`}>
<h2 className="text-md font-semibold truncate">{album.albumName}</h2>
</Link>
<p className="text-sm text-gray-700 dark:text-gray-500">{
numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`
}</p>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-gray-700 dark:text-gray-500">{humanizeNumber(album.faceCount)} {
pluralize(album.faceCount, 'person', 'people')
} </p>
<p className="text-xs text-gray-700 dark:text-gray-500">{humanizeBytes(parseInt(album.size))}</p>
</div>
</div>
</div>
)
+1 -1
View File
@@ -22,7 +22,7 @@ export const sidebarNavs = [
icon: <MapPinX className="h-4 w-4" />,
},
{
title: "Albums",
title: "Manage Albums",
link: "/albums",
icon: <GalleryHorizontal className="h-4 w-4" />,
},
+2 -2
View File
@@ -23,8 +23,8 @@ export const listPotentialAlbumsAssets = async (filters: IPotentialAlbumsDatesFi
return API.get(LIST_POTENTIAL_ALBUMS_ASSETS_PATH, filters).then((assets) => assets.map(cleanUpAsset));
}
export const listAlbums = async () => {
return API.get(LIST_ALBUMS_PATH);
export const listAlbums = async (filters?: { sortBy?: string, sortOrder?: string }) => {
return API.get(LIST_ALBUMS_PATH, filters);
}
export const getAlbumInfo = async (id: string) => {
return API.get(ALBUM_INFO_PATH(id));
+18 -1
View File
@@ -1,3 +1,20 @@
export const humanizeNumber = (number: number) => {
return number.toLocaleString();
}
}
export const pluralize = (number: number, word: string, pluralWord: string) => {
return number === 1 ? word : pluralWord
}
export const humanizeBytes = (bytes: number) => {
if (bytes < 1024) {
return `${bytes} bytes`
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`
}
if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
+58 -3
View File
@@ -7,20 +7,42 @@ 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, useState } from 'react'
import React, { useEffect, useMemo, 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'
const SORT_BY_OPTIONS = [
{ value: 'lastPhotoDate', label: 'Last Photo Date' },
{ value: 'firstPhotoDate', label: 'First Photo Date' },
{ value: 'assetCount', label: 'Asset Count' },
{ value: 'createdAt', label: 'Created At' },
{ value: 'updatedAt', label: 'Updated At' },
{ value: 'albumName', label: 'Album Name' },
{ value: 'albumSize', label: 'Album Size' },
{ value: 'faceCount', label: 'Number of People' },
]
export default function AlbumListPage() {
const { exImmichUrl } = useConfig()
const router = useRouter()
const { query, pathname } = router
const { sortBy, sortOrder } = query as { sortBy: string, sortOrder: string }
const [albums, setAlbums] = useState<IAlbum[]>([])
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [selectedAlbums, setSelectedAlbums] = useState<string[]>([])
const selectedSortBy = useMemo(() => SORT_BY_OPTIONS.find((option) => option.value === sortBy), [sortBy])
const fetchAlbums = async () => {
setLoading(true)
listAlbums()
listAlbums({
sortBy,
sortOrder
})
.then(setAlbums)
.catch((error) => {
setErrorMessage(error.message)
@@ -32,7 +54,7 @@ export default function AlbumListPage() {
useEffect(() => {
fetchAlbums()
}, [])
}, [sortBy, sortOrder])
const handleSelect = (checked: boolean, albumId: string) => {
if (checked) {
@@ -42,6 +64,7 @@ export default function AlbumListPage() {
}
}
const renderContent = () => {
if (loading) {
@@ -62,6 +85,38 @@ export default function AlbumListPage() {
<PageLayout className="!p-0 !mb-0">
<Header
leftComponent="Albums"
rightComponent={
<div className="flex items-center gap-2">
<Select
defaultValue={selectedSortBy?.value}
onValueChange={(value) => router.push({
pathname,
query: {
...query,
sortBy: value,
}
})}>
<SelectTrigger className="px-2">
<SelectValue placeholder="Sort by">{selectedSortBy?.label || 'Sort by'}</SelectValue>
</SelectTrigger>
<SelectContent>
{SORT_BY_OPTIONS.map((option) => (
<SelectItem value={option.value} key={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button className="px-2" variant={"secondary"} onClick={() => router.push({
pathname,
query: {
...query,
sortOrder: sortOrder === 'asc' ? 'desc' : 'asc',
}
})}>
{sortOrder === 'asc' ? <SortAsc /> : <SortDesc />}
</Button>
</div>
}
/>
{renderContent()}
</PageLayout>
+16 -1
View File
@@ -4,7 +4,7 @@ import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { NextApiResponse } from "next";
import { albums } from "@/schema/albums.schema";
import { count, desc, eq, min, max, sql, and } from "drizzle-orm";
import { count, desc, eq, min, max, sql, and, sum } from "drizzle-orm";
import { assets } from "@/schema/assets.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { users } from "@/schema/users.schema";
@@ -28,6 +28,15 @@ const sortAlbums = (albums: IAlbum[], sortBy: string, sortOrder: string) => {
if (sortBy === 'assetCount') {
return albums.sort((a, b) => sortOrder === 'asc' ? a.assetCount - b.assetCount : b.assetCount - a.assetCount);
}
if (sortBy === 'albumName') {
return albums.sort((a, b) => sortOrder === 'asc' ? a.albumName.localeCompare(b.albumName) : b.albumName.localeCompare(a.albumName));
}
if (sortBy === 'albumSize') {
return albums.sort((a, b) => sortOrder === 'asc' ? parseInt(a.size) - parseInt(b.size) : parseInt(b.size) - parseInt(a.size));
}
if (sortBy === 'faceCount') {
return albums.sort((a, b) => sortOrder === 'asc' ? a.faceCount - b.faceCount : b.faceCount - a.faceCount);
}
return albums;
}
export default async function handler(
@@ -49,6 +58,12 @@ export default async function handler(
assetCount: count(assets.id),
firstPhotoDate: min(exif.dateTimeOriginal),
lastPhotoDate: max(exif.dateTimeOriginal),
size: sum(exif.fileSizeInByte),
order: albums.order,
isActivityEnabled: albums.isActivityEnabled,
ownerId: albums.ownerId,
description: albums.description,
lastModifiedAssetTimestamp: max(exif.dateTimeOriginal),
faceCount: count(sql<string>`DISTINCT ${assetFaces.personId}`), // Ensure unique personId
})
.from(albums)
+8 -7
View File
@@ -6,13 +6,13 @@ export interface IAlbum {
updatedAt: Date;
id: string;
ownerId: string;
owner: IAlbumOwner;
albumUsers: any[];
shared: boolean;
hasSharedLink: boolean;
startDate: Date;
endDate: Date;
assets: any[];
owner?: IAlbumOwner;
albumUsers?: any[];
shared?: boolean;
hasSharedLink?: boolean;
startDate?: Date;
endDate?: Date;
assets?: any[];
assetCount: number;
isActivityEnabled: boolean;
order: string;
@@ -20,6 +20,7 @@ export interface IAlbum {
firstPhotoDate: Date;
lastPhotoDate: Date;
faceCount: number;
size: string;
}
export interface IAlbumOwner {