Deck actions UI, consistent icon buttons, native select fix, sidebar stats
- DeckView: rename/duplicate/delete deck via row-hover icon actions, with a ConfirmDialog for delete and PromptDialog (with friendly name- collision errors) for rename/duplicate. - Add a shared IconButton (plus overlay/danger variants) and replace every ad hoc close/remove button in the app with it: deck actions, remove-card-from-deck (list and tile), clear-commander, the card- preview close, and the ingest-progress dismiss. - Fix native <select> (Group by, Format, In deck): only color/border were overridden, leaving native OS chrome (height, arrow, popup) in place. Drop appearance and draw a custom chevron so they match text inputs. - Click-to-preview: show both faces of a double-faced card side by side on wide viewports instead of requiring Flip, and size the preview images larger overall. - Sidebar footer: replace the plain "N cards" line with a "Your library" panel (unique/copies/decks tiles + a WUBRG color-distribution bar), polling get_library_stats while card data is loaded. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,14 +5,29 @@ import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
ColorPips,
|
||||
ConfirmDialog,
|
||||
GroupBySelect,
|
||||
GroupMode,
|
||||
IconButton,
|
||||
Icons,
|
||||
PromptDialog,
|
||||
ViewToggle,
|
||||
groupCards,
|
||||
usePersistedState,
|
||||
useViewMode,
|
||||
} from "../components";
|
||||
|
||||
type DeckDialog =
|
||||
| { kind: "delete"; deck: Deck }
|
||||
| { kind: "rename"; deck: Deck }
|
||||
| { kind: "duplicate"; deck: Deck }
|
||||
| null;
|
||||
|
||||
function friendlyDeckError(err: unknown): string {
|
||||
const msg = String(err);
|
||||
return msg.includes("UNIQUE constraint failed") ? "A deck with that name already exists." : msg;
|
||||
}
|
||||
|
||||
const FORMATS = [
|
||||
"",
|
||||
"Commander",
|
||||
@@ -39,6 +54,8 @@ export function DeckView() {
|
||||
const [contents, setContents] = useState<DeckEntry[]>([]);
|
||||
const [view, setView] = useViewMode("deck-view");
|
||||
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("deck-group", "none");
|
||||
const [dialog, setDialog] = useState<DeckDialog>(null);
|
||||
const [dialogError, setDialogError] = useState<string | null>(null);
|
||||
|
||||
const [libQuery, setLibQuery] = useState("");
|
||||
const [libResults, setLibResults] = useState<CardRef[]>([]);
|
||||
@@ -121,12 +138,39 @@ export function DeckView() {
|
||||
await loadContents();
|
||||
}
|
||||
|
||||
async function removeDeck(id: number) {
|
||||
await api.deleteDeck(id);
|
||||
if (selected === id) setSelected(null);
|
||||
async function confirmDelete() {
|
||||
if (dialog?.kind !== "delete") return;
|
||||
await api.deleteDeck(dialog.deck.id);
|
||||
if (selected === dialog.deck.id) setSelected(null);
|
||||
setDialog(null);
|
||||
await loadDecks();
|
||||
}
|
||||
|
||||
async function confirmRename(name: string) {
|
||||
if (dialog?.kind !== "rename") return;
|
||||
try {
|
||||
await api.renameDeck(dialog.deck.id, name);
|
||||
setDialog(null);
|
||||
setDialogError(null);
|
||||
await loadDecks();
|
||||
} catch (err) {
|
||||
setDialogError(friendlyDeckError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDuplicate(newName: string) {
|
||||
if (dialog?.kind !== "duplicate") return;
|
||||
try {
|
||||
const d = await api.duplicateDeck(dialog.deck.id, newName);
|
||||
setDialog(null);
|
||||
setDialogError(null);
|
||||
await loadDecks();
|
||||
setSelected(d.id);
|
||||
} catch (err) {
|
||||
setDialogError(friendlyDeckError(err));
|
||||
}
|
||||
}
|
||||
|
||||
async function changeFormat(format: string) {
|
||||
if (selected === null) return;
|
||||
await api.setDeckFormat(selected, format);
|
||||
@@ -168,13 +212,12 @@ export function DeckView() {
|
||||
key={e.oracle_id}
|
||||
card={e}
|
||||
corner={
|
||||
<button
|
||||
className="tile-remove"
|
||||
title="Remove from deck"
|
||||
<IconButton
|
||||
icon={Icons.close}
|
||||
label="Remove from deck"
|
||||
overlay
|
||||
onClick={() => setQty(e.oracle_id, 0)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<span className="tile-badge">
|
||||
@@ -227,7 +270,11 @@ export function DeckView() {
|
||||
</td>
|
||||
<td>{e.quantity_owned}</td>
|
||||
<td>
|
||||
<button onClick={() => setQty(e.oracle_id, 0)}>Remove</button>
|
||||
<IconButton
|
||||
icon={Icons.close}
|
||||
label="Remove from deck"
|
||||
onClick={() => setQty(e.oracle_id, 0)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -275,15 +322,35 @@ export function DeckView() {
|
||||
<span className="card-name">{d.name}</span>
|
||||
{d.format && <span className="deck-chip">{d.format}</span>}
|
||||
<span className="spacer" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeDeck(d.id);
|
||||
}}
|
||||
title="Delete deck"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<span className="row-actions">
|
||||
<IconButton
|
||||
icon={Icons.pencil}
|
||||
label="Rename deck"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDialogError(null);
|
||||
setDialog({ kind: "rename", deck: d });
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={Icons.copy}
|
||||
label="Duplicate deck"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDialogError(null);
|
||||
setDialog({ kind: "duplicate", deck: d });
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={Icons.trash}
|
||||
label="Delete deck"
|
||||
danger
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDialog({ kind: "delete", deck: d });
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
{decks.length === 0 && (
|
||||
@@ -326,9 +393,7 @@ export function DeckView() {
|
||||
<span className="muted">Commander:</span>
|
||||
<strong>{commanderName || "none"}</strong>
|
||||
{commanderName && (
|
||||
<button onClick={clearCommander} title="Clear commander">
|
||||
✕
|
||||
</button>
|
||||
<IconButton icon={Icons.close} label="Clear commander" onClick={clearCommander} />
|
||||
)}
|
||||
</div>
|
||||
<CardSearchBox
|
||||
@@ -372,6 +437,39 @@ export function DeckView() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dialog?.kind === "delete" && (
|
||||
<ConfirmDialog
|
||||
title="Delete deck"
|
||||
message={`Delete "${dialog.deck.name}"? This can't be undone.`}
|
||||
confirmLabel="Delete"
|
||||
danger
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setDialog(null)}
|
||||
/>
|
||||
)}
|
||||
{dialog?.kind === "rename" && (
|
||||
<PromptDialog
|
||||
title="Rename deck"
|
||||
label="Deck name"
|
||||
defaultValue={dialog.deck.name}
|
||||
confirmLabel="Rename"
|
||||
error={dialogError}
|
||||
onConfirm={confirmRename}
|
||||
onCancel={() => setDialog(null)}
|
||||
/>
|
||||
)}
|
||||
{dialog?.kind === "duplicate" && (
|
||||
<PromptDialog
|
||||
title="Duplicate deck"
|
||||
label="Name for the copy"
|
||||
defaultValue={`${dialog.deck.name} (copy)`}
|
||||
confirmLabel="Duplicate"
|
||||
error={dialogError}
|
||||
onConfirm={confirmDuplicate}
|
||||
onCancel={() => setDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user