Files
k-photos/k-photos-frontend/hooks/use-upload.ts
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

91 lines
2.4 KiB
TypeScript

"use client"
import { useState, useCallback } from "react"
import { useQueryClient } from "@tanstack/react-query"
import api from "@/lib/api"
import { toast } from "sonner"
interface LibraryPathResponse {
id: string
is_ingest_destination: boolean
}
interface UploadProgress {
file: string
progress: number
done: boolean
}
export function useUpload(onComplete?: () => void) {
const qc = useQueryClient()
const [uploads, setUploads] = useState<UploadProgress[]>([])
const [isUploading, setIsUploading] = useState(false)
const upload = useCallback(
async (files: File[]) => {
setIsUploading(true)
const initial = files.map((f) => ({
file: f.name,
progress: 0,
done: false,
}))
setUploads(initial)
let targetPathId: string | null = null
try {
const { data } = await api.get<LibraryPathResponse[]>(
"/storage/library-paths",
)
const dest = data.find((p) => p.is_ingest_destination)
targetPathId = dest?.id ?? data[0]?.id ?? null
} catch {
toast.error("No ingest destination configured")
setIsUploading(false)
return
}
if (!targetPathId) {
toast.error("No ingest destination configured")
setIsUploading(false)
return
}
let succeeded = 0
let failed = 0
for (let i = 0; i < files.length; i++) {
try {
const form = new FormData()
form.append("file", files[i])
form.append("target_path_id", targetPathId)
await api.post("/assets/ingest", form, {
onUploadProgress: (e) => {
const pct = e.total ? Math.round((e.loaded * 100) / e.total) : 0
setUploads((prev) =>
prev.map((u, j) => (j === i ? { ...u, progress: pct } : u)),
)
},
})
setUploads((prev) =>
prev.map((u, j) =>
j === i ? { ...u, progress: 100, done: true } : u,
),
)
succeeded++
} catch {
failed++
}
}
if (succeeded > 0) toast.success(`Uploaded ${succeeded} file(s)`)
if (failed > 0) toast.error(`${failed} upload(s) failed`)
await qc.invalidateQueries({ queryKey: ["timeline"] })
setIsUploading(false)
onComplete?.()
},
[onComplete, qc],
)
return { uploads, isUploading, upload }
}