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).
This commit is contained in:
@@ -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> | 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 (
|
||||||
|
<>
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{albums.length === 0 ? (
|
||||||
|
<p className="px-2 py-2 text-[11px] text-gray-600">No albums yet — create one below.</p>
|
||||||
|
) : (
|
||||||
|
albums.map((album) => (
|
||||||
|
<button
|
||||||
|
key={album.id}
|
||||||
|
className="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-xs text-gray-300 transition-colors hover:bg-white/5 hover:text-white"
|
||||||
|
onClick={() => void onPick(album.id)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{album.name}</span>
|
||||||
|
<span className="shrink-0 text-[10px] text-gray-600">{album.image_count}</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="mt-1 flex gap-1 border-t border-white/[0.06] pt-2"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleCreate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-white placeholder-gray-600 focus:border-white/20 focus:outline-none"
|
||||||
|
placeholder="New album…"
|
||||||
|
value={newAlbumName}
|
||||||
|
onChange={(event) => setNewAlbumName(event.target.value)}
|
||||||
|
disabled={creating}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-50"
|
||||||
|
disabled={creating || !newAlbumName.trim()}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useGalleryStore } from "../store";
|
import { useGalleryStore } from "../store";
|
||||||
|
import { AlbumPicker } from "./AlbumPicker";
|
||||||
import { BulkTagPopover } from "./bulk/BulkTagPopover";
|
import { BulkTagPopover } from "./bulk/BulkTagPopover";
|
||||||
import { useDismissable } from "./menu";
|
import { useDismissable } from "./menu";
|
||||||
import { Tooltip } from "./Tooltip"
|
import { Tooltip } from "./Tooltip"
|
||||||
@@ -18,15 +19,11 @@ export function BulkActionBar() {
|
|||||||
const bulkDeleteSelected = useGalleryStore((state) => state.bulkDeleteSelected);
|
const bulkDeleteSelected = useGalleryStore((state) => state.bulkDeleteSelected);
|
||||||
const activeView = useGalleryStore((state) => state.activeView);
|
const activeView = useGalleryStore((state) => state.activeView);
|
||||||
const selectedAlbumId = useGalleryStore((state) => state.selectedAlbumId);
|
const selectedAlbumId = useGalleryStore((state) => state.selectedAlbumId);
|
||||||
const albums = useGalleryStore((state) => state.albums);
|
|
||||||
const addToAlbum = useGalleryStore((state) => state.addToAlbum);
|
const addToAlbum = useGalleryStore((state) => state.addToAlbum);
|
||||||
const removeFromAlbum = useGalleryStore((state) => state.removeFromAlbum);
|
const removeFromAlbum = useGalleryStore((state) => state.removeFromAlbum);
|
||||||
const createAlbum = useGalleryStore((state) => state.createAlbum);
|
|
||||||
|
|
||||||
const [panel, setPanel] = useState<Panel>(null);
|
const [panel, setPanel] = useState<Panel>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [creatingAlbum, setCreatingAlbum] = useState(false);
|
|
||||||
const [newAlbumName, setNewAlbumName] = useState("");
|
|
||||||
const barRef = useRef<HTMLDivElement>(null);
|
const barRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Close any open popover when clicking outside the bar or pressing Escape.
|
// 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.
|
// Reset transient UI whenever the selection empties.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) setPanel(null);
|
||||||
setPanel(null);
|
|
||||||
setNewAlbumName("");
|
|
||||||
}
|
|
||||||
}, [selectedCount]);
|
}, [selectedCount]);
|
||||||
|
|
||||||
if (selectedCount === 0) return null;
|
if (selectedCount === 0) return null;
|
||||||
@@ -61,20 +55,6 @@ export function BulkActionBar() {
|
|||||||
setPanel(null);
|
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 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 btnIdle = `${btn} text-gray-300 hover:bg-white/10 hover:text-white`;
|
||||||
const btnActive = `${btn} bg-white/10 text-white`;
|
const btnActive = `${btn} bg-white/10 text-white`;
|
||||||
@@ -162,44 +142,7 @@ export function BulkActionBar() {
|
|||||||
data-bulk-popover
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<AlbumPicker onPick={handlePickAlbum} />
|
||||||
{albums.length === 0 ? (
|
|
||||||
<p className="px-2 py-2 text-[11px] text-gray-600">No albums yet — create one below.</p>
|
|
||||||
) : (
|
|
||||||
albums.map((album) => (
|
|
||||||
<button
|
|
||||||
key={album.id}
|
|
||||||
className="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-xs text-gray-300 transition-colors hover:bg-white/5 hover:text-white"
|
|
||||||
onClick={() => void handlePickAlbum(album.id)}
|
|
||||||
>
|
|
||||||
<span className="truncate">{album.name}</span>
|
|
||||||
<span className="shrink-0 text-[10px] text-gray-600">{album.image_count}</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
className="mt-1 flex gap-1 border-t border-white/[0.06] pt-2"
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
void handleCreateAlbum();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="min-w-0 flex-1 rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-white placeholder-gray-600 focus:border-white/20 focus:outline-none"
|
|
||||||
placeholder="New album…"
|
|
||||||
value={newAlbumName}
|
|
||||||
onChange={(event) => setNewAlbumName(event.target.value)}
|
|
||||||
disabled={creatingAlbum}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-50"
|
|
||||||
disabled={creatingAlbum || !newAlbumName.trim()}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user