- 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
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import Link from "next/link"
|
|
import { usePathname } from "next/navigation"
|
|
import {
|
|
SidebarGroup,
|
|
SidebarGroupLabel,
|
|
SidebarGroupContent,
|
|
SidebarMenu,
|
|
SidebarMenuItem,
|
|
SidebarMenuButton,
|
|
} from "@/components/ui/sidebar"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { useAlbums } from "@/hooks/use-albums"
|
|
import { toast } from "sonner"
|
|
import { ImageIcon, PlusIcon, Trash2Icon } from "lucide-react"
|
|
|
|
export function AlbumSidebar() {
|
|
const pathname = usePathname()
|
|
const { albums, createAlbum, deleteAlbum } = useAlbums()
|
|
const [isCreating, setIsCreating] = useState(false)
|
|
const [newTitle, setNewTitle] = useState("")
|
|
|
|
const handleCreate = async () => {
|
|
if (!newTitle.trim()) return
|
|
await createAlbum(newTitle.trim()).catch(() => {})
|
|
setNewTitle("")
|
|
setIsCreating(false)
|
|
}
|
|
|
|
return (
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel className="flex items-center justify-between">
|
|
Albums
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5"
|
|
onClick={() => setIsCreating(!isCreating)}
|
|
>
|
|
<PlusIcon className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
{isCreating && (
|
|
<div className="px-2 pb-2">
|
|
<Input
|
|
autoFocus
|
|
placeholder="Album title"
|
|
value={newTitle}
|
|
onChange={(e) => setNewTitle(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") handleCreate()
|
|
if (e.key === "Escape") setIsCreating(false)
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<SidebarMenu>
|
|
{albums.map((album) => (
|
|
<SidebarMenuItem key={album.id} className="group/album">
|
|
<SidebarMenuButton
|
|
asChild
|
|
isActive={pathname === `/albums/${album.id}`}
|
|
>
|
|
<Link href={`/albums/${album.id}`}>
|
|
<ImageIcon className="h-4 w-4" />
|
|
<span className="flex-1">{album.title}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 opacity-0 group-hover/album:opacity-100"
|
|
onClick={async (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
try {
|
|
await deleteAlbum(album.id)
|
|
toast.success("Album deleted")
|
|
} catch {
|
|
toast.error("Failed to delete album")
|
|
}
|
|
}}
|
|
>
|
|
<Trash2Icon className="h-3 w-3" />
|
|
</Button>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
)
|
|
}
|