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,
|
||||
HoverPreviewProvider,
|
||||
IconButton,
|
||||
OpenDeckProvider,
|
||||
Icons,
|
||||
StatTile,
|
||||
UndoBar,
|
||||
@@ -241,6 +242,18 @@ export default function App() {
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [libStats, setLibStats] = useState<LibraryStats | null>(null);
|
||||
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 [busy, setBusy] = useState(false);
|
||||
// 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);
|
||||
|
||||
return (
|
||||
<OpenDeckProvider value={openDeck}>
|
||||
<HoverPreviewProvider>
|
||||
<div className="app">
|
||||
<nav className="nav-rail">
|
||||
@@ -763,7 +777,12 @@ export default function App() {
|
||||
) : tab === "search" ? (
|
||||
<CardSearch key={status?.library_label} onOfferUndo={offerUndo} />
|
||||
) : tab === "decks" ? (
|
||||
<DeckView key={status?.library_label} refreshSignal={deckRefreshSignal} />
|
||||
<DeckView
|
||||
key={status?.library_label}
|
||||
refreshSignal={deckRefreshSignal}
|
||||
openDeckId={openDeckId}
|
||||
onOpenedDeck={() => setOpenDeckId(null)}
|
||||
/>
|
||||
) : tab === "game" ? (
|
||||
// Like News, independent of the library: the game in progress
|
||||
// is this device's scratch state, not synced data.
|
||||
@@ -853,5 +872,6 @@ export default function App() {
|
||||
onRunUndo={runUndo}
|
||||
/>
|
||||
</HoverPreviewProvider>
|
||||
</OpenDeckProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -756,6 +756,40 @@ type ClickedState = { card: PreviewCard; ctx?: PreviewContext; showBack: boolean
|
||||
const HoverCtx = createContext<HoverApi | 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
|
||||
* tracks the cursor while hovering (see [`useHoverPreview`]), and a larger
|
||||
@@ -923,8 +957,8 @@ function LegalityPills({ legalities }: { legalities: number }) {
|
||||
* legality. */
|
||||
const CardInfoTile = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ card: PreviewCard; ctx?: PreviewContext; onArtChange: (art: CardArt) => void }
|
||||
>(function CardInfoTile({ card, ctx, onArtChange }, ref) {
|
||||
{ card: PreviewCard; ctx?: PreviewContext; onArtChange: (art: CardArt) => void; onClose: () => void }
|
||||
>(function CardInfoTile({ card, ctx, onArtChange, onClose }, ref) {
|
||||
const printings = !!ctx && card.quantity_owned !== undefined;
|
||||
return (
|
||||
<div className="preview-info" ref={ref}>
|
||||
@@ -954,9 +988,7 @@ const CardInfoTile = forwardRef<
|
||||
<span className="preview-info-label">In decks</span>
|
||||
<div className="preview-info-deck-chips">
|
||||
{card.decks.map((d) => (
|
||||
<span key={d} className="badge">
|
||||
{d}
|
||||
</span>
|
||||
<DeckChip key={d} name={d} className="badge" onOpen={onClose} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1397,7 +1429,7 @@ function ClickPreviewModal({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<CardInfoTile ref={infoRef} card={card} ctx={ctx} onArtChange={setArt} />
|
||||
<CardInfoTile ref={infoRef} card={card} ctx={ctx} onArtChange={setArt} onClose={onClose} />
|
||||
{compact && (
|
||||
<div className="preview-modal-bar">
|
||||
<PreviewPaneToggle hasBack={hasBack} showBack={showBack} pane={pane} onSelect={selectPane} />
|
||||
|
||||
@@ -1671,6 +1671,19 @@ tbody tr:hover {
|
||||
color: var(--text);
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { api, Deck, LibraryCard, LibraryFacets, PreviewContext } from "../api";
|
||||
import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
DeckChip,
|
||||
ColorPips,
|
||||
GroupBySelect,
|
||||
GroupMode,
|
||||
@@ -253,9 +254,7 @@ export function CardSearch({
|
||||
</td>
|
||||
<td data-label="In decks">
|
||||
{c.decks.map((d) => (
|
||||
<span key={d} className="deck-chip">
|
||||
{d}
|
||||
</span>
|
||||
<DeckChip key={d} name={d} />
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
@@ -310,11 +309,7 @@ export function CardSearch({
|
||||
}
|
||||
footer={
|
||||
c.decks.length > 0
|
||||
? c.decks.map((d) => (
|
||||
<span key={d} className="deck-chip">
|
||||
{d}
|
||||
</span>
|
||||
))
|
||||
? c.decks.map((d) => <DeckChip key={d} name={d} />)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -427,7 +427,16 @@ function computeDeckLegality(deck: Deck | null | undefined, contents: DeckEntry[
|
||||
* card list, mana curve, and legality tile recompute immediately instead of
|
||||
* 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 [selected, setSelected] = usePersistedState<number | null>(libraryKey("deck-selected"), null);
|
||||
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
|
||||
// both panes visible side by side above it regardless of this flag.
|
||||
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
|
||||
// its ← button. Only while they're separate screens (the ≤720px
|
||||
// `.deck-layout` breakpoint): side by side, flipping the flag changes
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
CardThumb,
|
||||
CardTile,
|
||||
DeckChip,
|
||||
DialogFrame,
|
||||
ColorPips,
|
||||
GroupBySelect,
|
||||
@@ -299,11 +300,7 @@ export function LibraryBrowser({
|
||||
onSaved={() => setRefreshKey((k) => k + 1)}
|
||||
/>
|
||||
) : (
|
||||
c.decks.map((d) => (
|
||||
<span key={d} className="deck-chip">
|
||||
{d}
|
||||
</span>
|
||||
))
|
||||
c.decks.map((d) => <DeckChip key={d} name={d} />)
|
||||
);
|
||||
|
||||
// Under the spares filter, how many copies are free — only worth saying for
|
||||
|
||||
Reference in New Issue
Block a user