From c27662dd744baad4794c6a507b4f8a38da9d0ded Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sat, 4 Jul 2026 12:11:36 +0100 Subject: [PATCH] refactor(ui): extract AlbumPicker from BulkActionBar The album popover body (album list + create-new-album form) moves to its own component. The host supplies onPick, which both an existing album row and a freshly created album route through -- BulkActionBar adds the selection and closes the panel. Ready for reuse anywhere else an album needs picking (e.g. the context menu submenu later). --- src/components/AlbumPicker.tsx | 70 ++++++++++++++++++++++++++++++++ src/components/BulkActionBar.tsx | 63 ++-------------------------- 2 files changed, 73 insertions(+), 60 deletions(-) create mode 100644 src/components/AlbumPicker.tsx diff --git a/src/components/AlbumPicker.tsx b/src/components/AlbumPicker.tsx new file mode 100644 index 0000000..9b95445 --- /dev/null +++ b/src/components/AlbumPicker.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { useGalleryStore } from "../store"; + +/** + * Album list plus a create-new-album form. The host decides what picking + * means — the bulk bar adds the current selection; a newly created album is + * picked immediately. + */ +export function AlbumPicker({ onPick }: { onPick: (albumId: number) => Promise | void }) { + const albums = useGalleryStore((state) => state.albums); + const createAlbum = useGalleryStore((state) => state.createAlbum); + const [creating, setCreating] = useState(false); + const [newAlbumName, setNewAlbumName] = useState(""); + + const handleCreate = async () => { + const name = newAlbumName.trim(); + if (!name || creating) return; + setCreating(true); + try { + const album = await createAlbum(name); + setNewAlbumName(""); + await onPick(album.id); + } finally { + setCreating(false); + } + }; + + return ( + <> +
+ {albums.length === 0 ? ( +

No albums yet — create one below.

+ ) : ( + albums.map((album) => ( + + )) + )} +
+
{ + event.preventDefault(); + void handleCreate(); + }} + > + setNewAlbumName(event.target.value)} + disabled={creating} + /> + +
+ + ); +} diff --git a/src/components/BulkActionBar.tsx b/src/components/BulkActionBar.tsx index a0340f5..d563bbe 100644 --- a/src/components/BulkActionBar.tsx +++ b/src/components/BulkActionBar.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useGalleryStore } from "../store"; +import { AlbumPicker } from "./AlbumPicker"; import { BulkTagPopover } from "./bulk/BulkTagPopover"; import { useDismissable } from "./menu"; import { Tooltip } from "./Tooltip" @@ -18,15 +19,11 @@ export function BulkActionBar() { const bulkDeleteSelected = useGalleryStore((state) => state.bulkDeleteSelected); const activeView = useGalleryStore((state) => state.activeView); const selectedAlbumId = useGalleryStore((state) => state.selectedAlbumId); - const albums = useGalleryStore((state) => state.albums); const addToAlbum = useGalleryStore((state) => state.addToAlbum); const removeFromAlbum = useGalleryStore((state) => state.removeFromAlbum); - const createAlbum = useGalleryStore((state) => state.createAlbum); const [panel, setPanel] = useState(null); const [deleting, setDeleting] = useState(false); - const [creatingAlbum, setCreatingAlbum] = useState(false); - const [newAlbumName, setNewAlbumName] = useState(""); const barRef = useRef(null); // Close any open popover when clicking outside the bar or pressing Escape. @@ -34,10 +31,7 @@ export function BulkActionBar() { // Reset transient UI whenever the selection empties. useEffect(() => { - if (selectedCount === 0) { - setPanel(null); - setNewAlbumName(""); - } + if (selectedCount === 0) setPanel(null); }, [selectedCount]); if (selectedCount === 0) return null; @@ -61,20 +55,6 @@ export function BulkActionBar() { setPanel(null); }; - const handleCreateAlbum = async () => { - const name = newAlbumName.trim(); - if (!name || creatingAlbum) return; - setCreatingAlbum(true); - try { - const album = await createAlbum(name); - await addToAlbum(album.id, ids); - setNewAlbumName(""); - setPanel(null); - } finally { - setCreatingAlbum(false); - } - }; - const btn = "rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors"; const btnIdle = `${btn} text-gray-300 hover:bg-white/10 hover:text-white`; const btnActive = `${btn} bg-white/10 text-white`; @@ -162,44 +142,7 @@ export function BulkActionBar() { data-bulk-popover className="absolute bottom-full left-1/2 mb-2 w-60 -translate-x-1/2 rounded-xl border border-white/10 bg-gray-950/98 p-2 shadow-2xl backdrop-blur" > -
- {albums.length === 0 ? ( -

No albums yet — create one below.

- ) : ( - albums.map((album) => ( - - )) - )} -
-
{ - event.preventDefault(); - void handleCreateAlbum(); - }} - > - setNewAlbumName(event.target.value)} - disabled={creatingAlbum} - /> - -
+ ) : null}