Deck chips open their deck
Deck-name chips in the Library, Card Search and the card preview modal are now buttons (DeckChip) that open that deck in the Decks tab — on mobile, straight to its building screen. App provides the open-deck callback via context and hands the deck id to DeckView. Closes #19
This commit is contained in:
22
src/App.tsx
22
src/App.tsx
@@ -16,6 +16,7 @@ import {
|
|||||||
formatRelativeTime,
|
formatRelativeTime,
|
||||||
HoverPreviewProvider,
|
HoverPreviewProvider,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
OpenDeckProvider,
|
||||||
Icons,
|
Icons,
|
||||||
StatTile,
|
StatTile,
|
||||||
UndoBar,
|
UndoBar,
|
||||||
@@ -241,6 +242,18 @@ export default function App() {
|
|||||||
const [status, setStatus] = useState<Status | null>(null);
|
const [status, setStatus] = useState<Status | null>(null);
|
||||||
const [libStats, setLibStats] = useState<LibraryStats | null>(null);
|
const [libStats, setLibStats] = useState<LibraryStats | null>(null);
|
||||||
const [tab, setTab] = usePersistedState<Tab>("active-tab", "add");
|
const [tab, setTab] = usePersistedState<Tab>("active-tab", "add");
|
||||||
|
// A deck a chip elsewhere asked to open; DeckView picks it up and clears it.
|
||||||
|
const [openDeckId, setOpenDeckId] = useState<number | null>(null);
|
||||||
|
// Deck chips only carry the deck's name, which is unique per library.
|
||||||
|
const openDeck = useCallback(
|
||||||
|
async (name: string) => {
|
||||||
|
const deck = (await api.listDecks()).find((d) => d.name === name);
|
||||||
|
if (!deck) return;
|
||||||
|
setOpenDeckId(deck.id);
|
||||||
|
setTab("decks");
|
||||||
|
},
|
||||||
|
[setTab],
|
||||||
|
);
|
||||||
const [progress, setProgress] = useState<IngestProgress | null>(null);
|
const [progress, setProgress] = useState<IngestProgress | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
// Library sync state: signed in or not, and how the last sync went. Fetched
|
// Library sync state: signed in or not, and how the last sync went. Fetched
|
||||||
@@ -598,6 +611,7 @@ export default function App() {
|
|||||||
if (status) setOpenLibrary(status.library.id);
|
if (status) setOpenLibrary(status.library.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<OpenDeckProvider value={openDeck}>
|
||||||
<HoverPreviewProvider>
|
<HoverPreviewProvider>
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<nav className="nav-rail">
|
<nav className="nav-rail">
|
||||||
@@ -763,7 +777,12 @@ export default function App() {
|
|||||||
) : tab === "search" ? (
|
) : tab === "search" ? (
|
||||||
<CardSearch key={status?.library_label} onOfferUndo={offerUndo} />
|
<CardSearch key={status?.library_label} onOfferUndo={offerUndo} />
|
||||||
) : tab === "decks" ? (
|
) : tab === "decks" ? (
|
||||||
<DeckView key={status?.library_label} refreshSignal={deckRefreshSignal} />
|
<DeckView
|
||||||
|
key={status?.library_label}
|
||||||
|
refreshSignal={deckRefreshSignal}
|
||||||
|
openDeckId={openDeckId}
|
||||||
|
onOpenedDeck={() => setOpenDeckId(null)}
|
||||||
|
/>
|
||||||
) : tab === "game" ? (
|
) : tab === "game" ? (
|
||||||
// Like News, independent of the library: the game in progress
|
// Like News, independent of the library: the game in progress
|
||||||
// is this device's scratch state, not synced data.
|
// is this device's scratch state, not synced data.
|
||||||
@@ -853,5 +872,6 @@ export default function App() {
|
|||||||
onRunUndo={runUndo}
|
onRunUndo={runUndo}
|
||||||
/>
|
/>
|
||||||
</HoverPreviewProvider>
|
</HoverPreviewProvider>
|
||||||
|
</OpenDeckProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,6 +756,40 @@ type ClickedState = { card: PreviewCard; ctx?: PreviewContext; showBack: boolean
|
|||||||
const HoverCtx = createContext<HoverApi | null>(null);
|
const HoverCtx = createContext<HoverApi | null>(null);
|
||||||
const ClickCtx = createContext<ClickApi | null>(null);
|
const ClickCtx = createContext<ClickApi | null>(null);
|
||||||
|
|
||||||
|
/** Opens a deck by name in the Decks tab. Provided by `App`; without it,
|
||||||
|
* deck chips render as plain labels. */
|
||||||
|
const OpenDeckCtx = createContext<((name: string) => void) | null>(null);
|
||||||
|
export const OpenDeckProvider = OpenDeckCtx.Provider;
|
||||||
|
|
||||||
|
/** A deck's name as a chip that opens the deck when clicked. `onOpen` runs
|
||||||
|
* first, e.g. to close the preview the chip sits in. */
|
||||||
|
export function DeckChip({
|
||||||
|
name,
|
||||||
|
className = "deck-chip",
|
||||||
|
onOpen,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
className?: string;
|
||||||
|
onOpen?: () => void;
|
||||||
|
}) {
|
||||||
|
const openDeck = useContext(OpenDeckCtx);
|
||||||
|
if (!openDeck) return <span className={className}>{name}</span>;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${className} deck-chip-link`}
|
||||||
|
title={`Open ${name}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpen?.();
|
||||||
|
openDeck(name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides card-image previews app-wide: a small floating preview that
|
* Provides card-image previews app-wide: a small floating preview that
|
||||||
* tracks the cursor while hovering (see [`useHoverPreview`]), and a larger
|
* tracks the cursor while hovering (see [`useHoverPreview`]), and a larger
|
||||||
@@ -923,8 +957,8 @@ function LegalityPills({ legalities }: { legalities: number }) {
|
|||||||
* legality. */
|
* legality. */
|
||||||
const CardInfoTile = forwardRef<
|
const CardInfoTile = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ card: PreviewCard; ctx?: PreviewContext; onArtChange: (art: CardArt) => void }
|
{ card: PreviewCard; ctx?: PreviewContext; onArtChange: (art: CardArt) => void; onClose: () => void }
|
||||||
>(function CardInfoTile({ card, ctx, onArtChange }, ref) {
|
>(function CardInfoTile({ card, ctx, onArtChange, onClose }, ref) {
|
||||||
const printings = !!ctx && card.quantity_owned !== undefined;
|
const printings = !!ctx && card.quantity_owned !== undefined;
|
||||||
return (
|
return (
|
||||||
<div className="preview-info" ref={ref}>
|
<div className="preview-info" ref={ref}>
|
||||||
@@ -954,9 +988,7 @@ const CardInfoTile = forwardRef<
|
|||||||
<span className="preview-info-label">In decks</span>
|
<span className="preview-info-label">In decks</span>
|
||||||
<div className="preview-info-deck-chips">
|
<div className="preview-info-deck-chips">
|
||||||
{card.decks.map((d) => (
|
{card.decks.map((d) => (
|
||||||
<span key={d} className="badge">
|
<DeckChip key={d} name={d} className="badge" onOpen={onClose} />
|
||||||
{d}
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1397,7 +1429,7 @@ function ClickPreviewModal({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CardInfoTile ref={infoRef} card={card} ctx={ctx} onArtChange={setArt} />
|
<CardInfoTile ref={infoRef} card={card} ctx={ctx} onArtChange={setArt} onClose={onClose} />
|
||||||
{compact && (
|
{compact && (
|
||||||
<div className="preview-modal-bar">
|
<div className="preview-modal-bar">
|
||||||
<PreviewPaneToggle hasBack={hasBack} showBack={showBack} pane={pane} onSelect={selectPane} />
|
<PreviewPaneToggle hasBack={hasBack} showBack={showBack} pane={pane} onSelect={selectPane} />
|
||||||
|
|||||||
@@ -1671,6 +1671,19 @@ tbody tr:hover {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
/* A deck chip that opens its deck (see `DeckChip`): keeps the chip's own
|
||||||
|
look rather than the global button's, plus a hover cue. */
|
||||||
|
.deck-chip-link {
|
||||||
|
line-height: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.deck-chip.deck-chip-link {
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
.deck-chip-link:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Bulk-selection action bar (Library tab)
|
Bulk-selection action bar (Library tab)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { api, Deck, LibraryCard, LibraryFacets, PreviewContext } from "../api";
|
|||||||
import {
|
import {
|
||||||
CardThumb,
|
CardThumb,
|
||||||
CardTile,
|
CardTile,
|
||||||
|
DeckChip,
|
||||||
ColorPips,
|
ColorPips,
|
||||||
GroupBySelect,
|
GroupBySelect,
|
||||||
GroupMode,
|
GroupMode,
|
||||||
@@ -253,9 +254,7 @@ export function CardSearch({
|
|||||||
</td>
|
</td>
|
||||||
<td data-label="In decks">
|
<td data-label="In decks">
|
||||||
{c.decks.map((d) => (
|
{c.decks.map((d) => (
|
||||||
<span key={d} className="deck-chip">
|
<DeckChip key={d} name={d} />
|
||||||
{d}
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -310,11 +309,7 @@ export function CardSearch({
|
|||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
c.decks.length > 0
|
c.decks.length > 0
|
||||||
? c.decks.map((d) => (
|
? c.decks.map((d) => <DeckChip key={d} name={d} />)
|
||||||
<span key={d} className="deck-chip">
|
|
||||||
{d}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -427,7 +427,16 @@ function computeDeckLegality(deck: Deck | null | undefined, contents: DeckEntry[
|
|||||||
* card list, mana curve, and legality tile recompute immediately instead of
|
* card list, mana curve, and legality tile recompute immediately instead of
|
||||||
* only on the next mount.
|
* only on the next mount.
|
||||||
*/
|
*/
|
||||||
export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
|
export function DeckView({
|
||||||
|
refreshSignal,
|
||||||
|
openDeckId,
|
||||||
|
onOpenedDeck,
|
||||||
|
}: {
|
||||||
|
refreshSignal?: number;
|
||||||
|
/** A deck to open, e.g. from a deck chip elsewhere; `onOpenedDeck` clears it. */
|
||||||
|
openDeckId?: number | null;
|
||||||
|
onOpenedDeck?: () => void;
|
||||||
|
}) {
|
||||||
const [decks, setDecks] = useState<Deck[]>([]);
|
const [decks, setDecks] = useState<Deck[]>([]);
|
||||||
const [selected, setSelected] = usePersistedState<number | null>(libraryKey("deck-selected"), null);
|
const [selected, setSelected] = usePersistedState<number | null>(libraryKey("deck-selected"), null);
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
@@ -444,6 +453,14 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
|
|||||||
// this only matters below the .deck-layout stacking breakpoint; CSS keeps
|
// this only matters below the .deck-layout stacking breakpoint; CSS keeps
|
||||||
// both panes visible side by side above it regardless of this flag.
|
// both panes visible side by side above it regardless of this flag.
|
||||||
const [mobileShowList, setMobileShowList] = useState(true);
|
const [mobileShowList, setMobileShowList] = useState(true);
|
||||||
|
|
||||||
|
// Open a deck asked for from outside — straight to its building screen.
|
||||||
|
useEffect(() => {
|
||||||
|
if (openDeckId == null) return;
|
||||||
|
setSelected(openDeckId);
|
||||||
|
setMobileShowList(false);
|
||||||
|
onOpenedDeck?.();
|
||||||
|
}, [openDeckId]);
|
||||||
// Android back from the deck-building screen returns to the deck list, like
|
// Android back from the deck-building screen returns to the deck list, like
|
||||||
// its ← button. Only while they're separate screens (the ≤720px
|
// its ← button. Only while they're separate screens (the ≤720px
|
||||||
// `.deck-layout` breakpoint): side by side, flipping the flag changes
|
// `.deck-layout` breakpoint): side by side, flipping the flag changes
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CardThumb,
|
CardThumb,
|
||||||
CardTile,
|
CardTile,
|
||||||
|
DeckChip,
|
||||||
DialogFrame,
|
DialogFrame,
|
||||||
ColorPips,
|
ColorPips,
|
||||||
GroupBySelect,
|
GroupBySelect,
|
||||||
@@ -299,11 +300,7 @@ export function LibraryBrowser({
|
|||||||
onSaved={() => setRefreshKey((k) => k + 1)}
|
onSaved={() => setRefreshKey((k) => k + 1)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
c.decks.map((d) => (
|
c.decks.map((d) => <DeckChip key={d} name={d} />)
|
||||||
<span key={d} className="deck-chip">
|
|
||||||
{d}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Under the spares filter, how many copies are free — only worth saying for
|
// Under the spares filter, how many copies are free — only worth saying for
|
||||||
|
|||||||
Reference in New Issue
Block a user