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
This commit is contained in:
103
k-photos-frontend/components/asset-picker-dialog.tsx
Normal file
103
k-photos-frontend/components/asset-picker-dialog.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user