Files
k-photos/k-photos-frontend/components/add-to-album-dialog.tsx
Gabriel Kaszewski 957737ac9b feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer
Backend:
- user roles (DB + JWT + first-user-is-admin)
- volume-aware file resolver (multi-volume asset serving)
- directory scanner uses volume URI directly
- date-summary endpoint (capture date from EXIF)
- timeline ordered by capture date
- list endpoints: volumes, plugins, pipelines, library paths
- delete endpoints: volumes, library paths
- configurable upload body limit (MAX_UPLOAD_BYTES)

Frontend:
- auth: login/register, token refresh, role-based admin gate
- timeline: date-grouped grid, infinite scroll, date scrubber
- image viewer: fullscreen zoom/pan/pinch, metadata sidebar
- upload: drag-drop, sequential upload, progress tracking
- albums: create, add/remove photos, asset picker dialog
- admin: storage (import library), jobs (pagination, error details),
  plugins (list + toggle), pipelines, sidecars, duplicates
- multi-select mode with add-to-album action
- TanStack Query for all data fetching
2026-06-01 01:35:43 +02:00

112 lines
3.0 KiB
TypeScript

"use client"
import { useState } from "react"
import { useAlbums } from "@/hooks/use-albums"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"
import { toast } from "sonner"
import { PlusIcon } from "lucide-react"
interface AddToAlbumDialogProps {
assetIds: string[]
open: boolean
onOpenChange: (open: boolean) => void
}
export function AddToAlbumDialog({
assetIds,
open,
onOpenChange,
}: AddToAlbumDialogProps) {
const { albums, isLoading, createAlbum, addEntry } = useAlbums()
const [newTitle, setNewTitle] = useState("")
const [adding, setAdding] = useState(false)
const handleAdd = async (albumId: string) => {
setAdding(true)
try {
for (const assetId of assetIds) {
await addEntry({ albumId, assetId }).catch(() => {})
}
toast.success(`Added ${assetIds.length} photo(s) to album`)
onOpenChange(false)
} catch {
toast.error("Failed to add to album")
} finally {
setAdding(false)
}
}
const handleCreateAndAdd = async () => {
if (!newTitle.trim()) return
setAdding(true)
try {
const album = await createAlbum(newTitle.trim())
for (const assetId of assetIds) {
await addEntry({ albumId: album.id, assetId }).catch(() => {})
}
setNewTitle("")
toast.success(`Created album and added ${assetIds.length} photo(s)`)
onOpenChange(false)
} catch {
toast.error("Failed")
} finally {
setAdding(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Add to Album</DialogTitle>
</DialogHeader>
<div className="flex min-h-0 flex-col gap-2 overflow-y-auto">
{isLoading ? (
<Spinner />
) : (
albums.map((album) => (
<Button
key={album.id}
variant="outline"
className="justify-start"
disabled={adding}
onClick={() => handleAdd(album.id)}
>
{album.title}
<span className="ml-auto text-xs text-muted-foreground">
{album.asset_count}
</span>
</Button>
))
)}
</div>
<div className="flex gap-2 border-t pt-3">
<Input
placeholder="New album name"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleCreateAndAdd()}
className="h-8"
/>
<Button
size="sm"
disabled={!newTitle.trim() || adding}
onClick={handleCreateAndAdd}
>
<PlusIcon className="mr-1 h-3 w-3" />
Create
</Button>
</div>
</DialogContent>
</Dialog>
)
}