Files
k-photos/k-photos-frontend/components/asset-picker-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

104 lines
2.7 KiB
TypeScript

"use client"
import { useState, useCallback } from "react"
import { useTimeline } from "@/hooks/use-timeline"
import { PhotoCard } from "./photo-card"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Spinner } from "@/components/ui/spinner"
interface AssetPickerDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
excludeIds?: Set<string>
onConfirm: (assetIds: string[]) => void
}
export function AssetPickerDialog({
open,
onOpenChange,
excludeIds,
onConfirm,
}: AssetPickerDialogProps) {
const { assets, isLoading, hasMore, loadMore } = useTimeline()
const [selected, setSelected] = useState<Set<string>>(new Set())
const toggle = useCallback((id: string, sel: boolean) => {
setSelected((prev) => {
const next = new Set(prev)
if (sel) next.add(id)
else next.delete(id)
return next
})
}, [])
const filtered = excludeIds
? assets.filter((a) => !excludeIds.has(a.id))
: assets
const handleConfirm = () => {
onConfirm(Array.from(selected))
setSelected(new Set())
onOpenChange(false)
}
return (
<Dialog
open={open}
onOpenChange={(o) => {
if (!o) setSelected(new Set())
onOpenChange(o)
}}
>
<DialogContent className="flex flex-col sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Select Photos</DialogTitle>
</DialogHeader>
<ScrollArea className="min-h-0 flex-1">
<div className="grid grid-cols-3 gap-1 sm:grid-cols-4 md:grid-cols-5">
{filtered.map((asset) => (
<PhotoCard
key={asset.id}
asset={asset}
selectable
selected={selected.has(asset.id)}
onSelect={(sel) => toggle(asset.id, sel)}
/>
))}
</div>
{hasMore && (
<div className="flex justify-center py-3">
<Button
size="sm"
variant="ghost"
onClick={() => loadMore()}
disabled={isLoading}
>
{isLoading ? <Spinner /> : "Load more"}
</Button>
</div>
)}
</ScrollArea>
<div className="flex items-center justify-between border-t pt-3">
<span className="text-sm text-muted-foreground">
{selected.size} selected
</span>
<Button
size="sm"
disabled={selected.size === 0}
onClick={handleConfirm}
>
Add to Album
</Button>
</div>
</DialogContent>
</Dialog>
)
}