Files
k-photos/libertas-frontend/src/routes/albums/$albumId.tsx
Gabriel Kaszewski 94b184d3b0 feat: Implement person management features
- Added hooks for listing, creating, updating, deleting, sharing, and merging people.
- Introduced a new route for person details and media.
- Implemented clustering faces functionality.
- Created services for person-related API interactions.

feat: Introduce tag management functionality

- Added hooks for listing, adding, and removing tags from media.
- Created services for tag-related API interactions.

feat: Enhance user authentication handling

- Added a hook to fetch current user details.
- Updated auth storage to manage user state more effectively.

feat: Update album management features

- Enhanced album service to return created album details.
- Updated API handlers to return album responses upon creation.
- Modified album repository to return created album.

feat: Implement media management improvements

- Added media details fetching and processing of media URLs.
- Enhanced media upload functionality to return processed media.

feat: Introduce face management features

- Added services for listing faces for media and assigning faces to persons.

fix: Update API client to clear authentication state on 401 errors.
2025-11-16 02:24:50 +01:00

110 lines
3.4 KiB
TypeScript

import { AddMediaToAlbumDialog } from "@/components/albums/add-media-to-album-dialog";
import { AuthenticatedImage } from "@/components/media/authenticated-image";
import { MediaViewer } from "@/components/media/media-viewer";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import type { Media } from "@/domain/types";
import {
useGetAlbum,
useGetAlbumMedia,
useRemoveMediaFromAlbum,
} from "@/features/albums/use-albums";
import { createFileRoute } from "@tanstack/react-router";
import { Eye, Trash2 } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/albums/$albumId")({
component: AlbumDetailPage,
});
function AlbumDetailPage() {
const { albumId } = Route.useParams();
const {
data: album,
isLoading: isLoadingAlbum,
error: albumError,
} = useGetAlbum(albumId);
const {
data: media,
isLoading: isLoadingMedia,
error: mediaError,
} = useGetAlbumMedia(albumId);
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
const { mutate: removeMedia, isPending: isRemoving } =
useRemoveMediaFromAlbum(albumId);
const isLoading = isLoadingAlbum || isLoadingMedia;
const error = albumError || mediaError;
const handleRemoveMedia = (mediaId: string) => {
removeMedia({
media_ids: [mediaId],
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold truncate">
{album?.name ?? "Loading album..."}
</h1>
<AddMediaToAlbumDialog albumId={albumId} />
</div>
{isLoading && <p>Loading photos...</p>}
{error && <p>Error loading photos: {error.message}</p>}
{media && media.length > 0 && (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
{media.map((m) => (
<ContextMenu key={m.id}>
<ContextMenuTrigger>
<div
className="aspect-square bg-gray-200 rounded-md overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setSelectedMedia(m)}
>
<AuthenticatedImage
src={m.thumbnail_url ?? m.file_url}
alt={m.original_filename}
className="w-full h-full object-cover"
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => setSelectedMedia(m)}>
<Eye className="mr-2 h-4 w-4" />
View
</ContextMenuItem>
<ContextMenuItem
className="text-destructive focus:text-destructive"
onSelect={() => handleRemoveMedia(m.id)}
disabled={isRemoving}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove from Album
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
</div>
)}
{media && media.length === 0 && <p>This album is empty.</p>}
<MediaViewer
media={selectedMedia}
onOpenChange={(open) => {
if (!open) {
setSelectedMedia(null);
}
}}
/>
</div>
);
}