Files
k-photos/k-photos-frontend/app/(app)/trash/page.tsx
Gabriel Kaszewski 0077caa743 feat: safe deletion, album/asset delete, trash, README update
- volume-aware deletion: read-only volumes remove DB only, writable
  volumes soft-delete to trash with configurable grace period
- trash page with restore, worker purge sweep (TRASH_RETENTION_DAYS)
- album delete endpoint + sidebar trash icon
- asset delete from timeline selection toolbar
- all listing queries exclude trashed assets (deleted_at IS NULL)
- timeline ordered by EXIF capture date, date-summary endpoint
- README rewritten with features, setup, full env var table
2026-06-01 01:57:53 +02:00

88 lines
2.7 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"
import api from "@/lib/api"
import type { TimelineResponse, AssetResponse } from "@/lib/types"
import { PhotoCard } from "@/components/photo-card"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { toast } from "sonner"
import { RotateCcwIcon } from "lucide-react"
export default function TrashPage() {
const qc = useQueryClient()
const { data, isLoading } = useQuery({
queryKey: ["trash"],
queryFn: async () => {
const { data } = await api.get<TimelineResponse>("/assets/trash", {
params: { limit: 100, offset: 0 },
})
return data
},
})
const restore = useMutation({
mutationFn: async (assetId: string) => {
await api.post(`/assets/${assetId}/restore`)
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["trash"] })
qc.invalidateQueries({ queryKey: ["timeline"] })
qc.invalidateQueries({ queryKey: ["date-summary"] })
},
})
const assets = data?.assets ?? []
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h1 className="text-lg font-semibold">Trash</h1>
{data && data.total > 0 && (
<span className="text-sm text-muted-foreground">
{data.total} items
</span>
)}
</div>
{isLoading ? (
<Spinner />
) : assets.length === 0 ? (
<p className="py-12 text-center text-muted-foreground">
Trash is empty
</p>
) : (
<div className="grid grid-cols-2 gap-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{assets.map((asset) => (
<div key={asset.id} className="group relative">
<div className="opacity-60">
<PhotoCard asset={asset} />
</div>
<div className="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100">
<Button
size="icon"
variant="secondary"
className="h-7 w-7"
disabled={restore.isPending}
onClick={async () => {
try {
await restore.mutateAsync(asset.id)
toast.success("Photo restored")
} catch {
toast.error("Failed to restore")
}
}}
>
<RotateCcwIcon className="h-3.5 w-3.5" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
)
}