Deck builder: sorting and a Price column

Adds the Library's Sort by control (plus a Quantity in deck option) to the
deck builder, applied within each section and group. The card table gets
an optional Price column showing the per-copy price, with the total for
the deck's copies in its tooltip. The toolbar now wraps on narrow screens.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 21:25:15 -05:00
parent 70b4d5427b
commit 1e0cd50c7d
3 changed files with 53 additions and 9 deletions

View File

@@ -22,6 +22,7 @@ import {
ColumnDef,
ColumnsMenu,
ConfirmDialog,
DECK_SORT_OPTIONS,
DialogFrame,
GroupBySelect,
GroupMode,
@@ -32,10 +33,14 @@ import {
PromptDialog,
QtyStepper,
SampleHandModal,
SortBySelect,
SortDir,
SortMode,
StatTile,
TileColumnsControl,
ViewToggle,
groupCards,
sortCards,
useClickPreview,
useHoverPreview,
useColumns,
@@ -73,7 +78,7 @@ function boardToggleProps(e: DeckEntry) {
: { icon: Icons.helpCircle, label: "Move to maybeboard" };
}
type DeckColumn = "image" | "colors" | "type" | "qty" | "owned" | "where" | "actions";
type DeckColumn = "image" | "colors" | "type" | "qty" | "owned" | "price" | "where" | "actions";
// The List view's optional columns; the checkbox and name always show.
const DECK_COLUMNS: ColumnDef<DeckColumn>[] = [
@@ -82,6 +87,7 @@ const DECK_COLUMNS: ColumnDef<DeckColumn>[] = [
{ id: "type", label: "Type" },
{ id: "qty", label: "In deck" },
{ id: "owned", label: "Owned" },
{ id: "price", label: "Price" },
{ id: "where", label: "Where" },
{ id: "actions", label: "Actions" },
];
@@ -342,6 +348,8 @@ function DeckBuilder({
const tiles = useTileColumns();
const columns = useColumns("deck-columns", DECK_COLUMNS);
const [groupBy, setGroupBy] = usePersistedState<GroupMode>("deck-group", "none");
const [sortBy, setSortBy] = usePersistedState<SortMode>("deck-sort", "name");
const [sortDir, setSortDir] = usePersistedState<SortDir>("deck-sort-dir", "asc");
const [combineSections, setCombineSections] = usePersistedState("deck-combine-sections", false);
const [filterQuery, setFilterQuery] = useState("");
@@ -994,6 +1002,7 @@ function DeckBuilder({
{columns.shown("type") && <th>Type</th>}
{columns.shown("qty") && <th>In deck</th>}
{columns.shown("owned") && <th>Owned</th>}
{columns.shown("price") && <th>Price</th>}
{columns.shown("where") && <th>Where</th>}
{columns.shown("actions") && <th></th>}
</tr>
@@ -1040,6 +1049,19 @@ function DeckBuilder({
</td>
)}
{columns.shown("owned") && <td data-label="Owned">{e.quantity_owned}</td>}
{columns.shown("price") && (
<td
data-label="Price"
className="cell-price"
title={
e.price_usd > 0 && e.quantity > 1
? `${formatCost(e.price_usd * e.quantity)} for ${e.quantity} copies`
: undefined
}
>
{e.price_usd > 0 ? `$${e.price_usd.toFixed(2)}` : "—"}
</td>
)}
{columns.shown("where") && <td data-label="Where">{whereabouts(e)}</td>}
{columns.shown("actions") && (
<td>
@@ -1060,7 +1082,7 @@ function DeckBuilder({
function cardsGroup(entries: DeckEntry[]) {
if (entries.length === 0) return <p className="hint">None yet.</p>;
return groupCards(entries, groupBy).map((g) => (
return groupCards(sortCards(entries, sortBy, sortDir), groupBy).map((g) => (
<div key={g.label || "all"} style={{ marginBottom: g.label ? 14 : 0 }}>
{g.label && (
<h4 className="group-header">
@@ -1277,7 +1299,7 @@ function DeckBuilder({
<hr className="deck-section-divider" />
<div className="row" style={{ gap: 16 }}>
<div className="row" style={{ gap: 16, flexWrap: "wrap" }}>
<label className="row" style={{ gap: 6, cursor: "pointer" }} title="Select all cards">
<input
ref={selectAllRef}
@@ -1302,6 +1324,13 @@ function DeckBuilder({
</label>
<span className="spacer" />
<GroupBySelect value={groupBy} onChange={setGroupBy} />
<SortBySelect
value={sortBy}
dir={sortDir}
onChangeValue={setSortBy}
onToggleDir={() => setSortDir(sortDir === "asc" ? "desc" : "asc")}
options={DECK_SORT_OPTIONS}
/>
<ViewToggle mode={view} onChange={setView} />
{view === "list" && <ColumnsMenu columns={columns} />}
{view === "tiles" && <TileColumnsControl tiles={tiles} />}