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.
This commit is contained in:
2025-11-16 02:24:50 +01:00
parent f41a3169e9
commit 94b184d3b0
34 changed files with 1300 additions and 281 deletions

View File

@@ -33,7 +33,7 @@ export function AddMediaToAlbumDialog({ albumId }: AddMediaToAlbumDialogProps) {
isFetchingNextPage,
} = useGetMediaList();
const { mutate: addMedia, isPending: isAdding } = useAddMediaToAlbum();
const { mutate: addMedia, isPending: isAdding } = useAddMediaToAlbum(albumId);
const toggleSelection = (mediaId: string) => {
setSelectedMediaIds((prev) =>
@@ -46,8 +46,7 @@ export function AddMediaToAlbumDialog({ albumId }: AddMediaToAlbumDialogProps) {
const handleSubmit = () => {
addMedia(
{
albumId,
payload: { media_ids: selectedMediaIds },
media_ids: selectedMediaIds,
},
{
onSuccess: () => {

View File

@@ -4,11 +4,11 @@ import { cn } from "@/lib/utils";
import { useAuthStorage } from "@/hooks/use-auth-storage";
export function Sidebar() {
const { user, clearToken } = useAuthStorage();
const { user, clearAuth } = useAuthStorage();
const navigate = useNavigate();
const handleLogout = () => {
clearToken();
clearAuth();
navigate({ to: "/login" });
};
@@ -32,7 +32,7 @@ export function Sidebar() {
</SidebarLink>
</div>
<div className="p-4 border-t border-gray-700">
<p className="text-sm text-gray-400">{user?.email}</p>
<p className="text-sm text-gray-400">{user?.email ?? "Loading..."}</p>
<button
onClick={handleLogout}
className="w-full mt-2 text-left text-red-400 hover:text-red-300"

View File

@@ -0,0 +1,88 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useUpdatePerson } from "@/features/people/use-people";
import { type Person } from "@/domain/types";
import { Pencil } from "lucide-react";
type EditPersonDialogProps = {
person: Person;
};
export function EditPersonDialog({ person }: EditPersonDialogProps) {
const [isOpen, setIsOpen] = useState(false);
const { mutate: updatePerson, isPending } = useUpdatePerson(person.id);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const name = formData.get("name") as string;
if (name) {
updatePerson(
{ name },
{
onSuccess: () => {
setIsOpen(false);
},
}
);
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Pencil size={18} />
<span className="sr-only">Edit Name</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Edit Person</DialogTitle>
<DialogDescription>
Change the name for this person.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
name="name"
defaultValue={person.name}
required
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,102 @@
import { useState } from "react";
import { type Person } from "@/domain/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useNavigate } from "@tanstack/react-router";
import { Trash2, UserSquare } from "lucide-react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useDeletePerson } from "@/features/people/use-people";
import { buttonVariants } from "@/components/ui/button";
type PersonCardProps = {
person: Person;
};
export function PersonCard({ person }: PersonCardProps) {
const navigate = useNavigate();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutate: deletePerson, isPending: isDeleting } = useDeletePerson(
person.id
);
const handleDelete = () => {
deletePerson();
};
return (
<>
<ContextMenu>
<ContextMenuTrigger>
<Card
className="overflow-hidden hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => {
// Navigate on left click
navigate({
to: "/people/$personId",
params: { personId: person.id },
});
}}
>
<CardHeader className="p-0">
<div className="aspect-square bg-gray-200 flex items-center justify-center">
{/* TODO: Add person thumbnail */}
<UserSquare className="w-1/2 h-1/2 text-gray-400" />
</div>
</CardHeader>
<CardContent className="p-4">
<CardTitle className="text-lg truncate text-center">
{person.name}
</CardTitle>
</CardContent>
</Card>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
className="text-destructive focus:text-destructive"
onSelect={() => setShowDeleteDialog(true)}
disabled={isDeleting}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Person
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete{" "}
<strong>{person.name}</strong> and unassign all associated faces.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}