👁️
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix deck stats positioning

+178 -139
+1 -1
src/components/deck/CardPreviewPane.tsx
··· 11 11 const { data } = useQuery(getCardByIdQueryOptions(cardId)); 12 12 13 13 return ( 14 - <div className="sticky top-20 flex items-center justify-center"> 14 + <div className="flex items-center justify-center"> 15 15 {cardId && ( 16 16 <CardImage 17 17 card={{ id: cardId, name: data?.name ?? "" }}
+49 -128
src/components/deck/DeckStats.tsx
··· 1 - import { useQueries } from "@tanstack/react-query"; 2 - import { useMemo, useState } from "react"; 3 - import { 4 - computeManaCurve, 5 - computeManaSymbolsVsSources, 6 - computeSpeedDistribution, 7 - computeSubtypeDistribution, 8 - computeTypeDistribution, 9 - } from "@/lib/deck-stats"; 10 - import type { DeckCard } from "@/lib/deck-types"; 11 - import { combineCardQueries, getCardByIdQueryOptions } from "@/lib/queries"; 12 - import type { ScryfallId } from "@/lib/scryfall-types"; 13 - import { getSelectedCards, type StatsSelection } from "@/lib/stats-selection"; 1 + import type { StatsSelection } from "@/lib/stats-selection"; 2 + import type { DeckStatsData } from "@/lib/useDeckStats"; 14 3 import { ManaBreakdown } from "./stats/ManaBreakdown"; 15 4 import { ManaCurveChart } from "./stats/ManaCurveChart"; 16 5 import { SpeedPieChart } from "./stats/SpeedPieChart"; 17 - import { StatsCardList } from "./stats/StatsCardList"; 18 6 import { SubtypesPieChart } from "./stats/SubtypesPieChart"; 19 7 import { TypesPieChart } from "./stats/TypesPieChart"; 20 8 21 9 interface DeckStatsProps { 22 - cards: DeckCard[]; 23 - onCardHover: (cardId: ScryfallId | null) => void; 24 - onCardClick?: (card: DeckCard) => void; 10 + stats: DeckStatsData; 11 + selection: StatsSelection; 12 + onSelect: (selection: StatsSelection) => void; 25 13 } 26 14 27 - export function DeckStats({ cards, onCardHover, onCardClick }: DeckStatsProps) { 28 - const cardMap = useQueries({ 29 - queries: cards.map((card) => getCardByIdQueryOptions(card.scryfallId)), 30 - combine: combineCardQueries, 31 - }); 32 - 33 - const [selection, setSelection] = useState<StatsSelection>(null); 15 + export function DeckStats({ stats, selection, onSelect }: DeckStatsProps) { 16 + const { 17 + manaCurve, 18 + typeDistribution, 19 + subtypeDistribution, 20 + speedDistribution, 21 + manaBreakdown, 22 + isLoading, 23 + } = stats; 34 24 35 - // Compute all statistics 36 - const manaCurve = useMemo( 37 - () => 38 - cardMap 39 - ? computeManaCurve(cards, (dc) => cardMap.get(dc.scryfallId)) 40 - : [], 41 - [cards, cardMap], 42 - ); 43 - 44 - const typeDistribution = useMemo( 45 - () => 46 - cardMap 47 - ? computeTypeDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 48 - : [], 49 - [cards, cardMap], 50 - ); 51 - 52 - const subtypeDistribution = useMemo( 53 - () => 54 - cardMap 55 - ? computeSubtypeDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 56 - : [], 57 - [cards, cardMap], 58 - ); 59 - 60 - const speedDistribution = useMemo( 61 - () => 62 - cardMap 63 - ? computeSpeedDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 64 - : [], 65 - [cards, cardMap], 66 - ); 67 - 68 - const manaBreakdown = useMemo( 69 - () => 70 - cardMap 71 - ? computeManaSymbolsVsSources(cards, (dc) => cardMap.get(dc.scryfallId)) 72 - : [], 73 - [cards, cardMap], 74 - ); 75 - 76 - // Derive selected cards from selection + stats 77 - const { cards: selectedCards, title: selectedTitle } = useMemo( 78 - () => 79 - getSelectedCards(selection, { 80 - manaCurve, 81 - typeDistribution, 82 - subtypeDistribution, 83 - speedDistribution, 84 - manaBreakdown, 85 - }), 86 - [ 87 - selection, 88 - manaCurve, 89 - typeDistribution, 90 - subtypeDistribution, 91 - speedDistribution, 92 - manaBreakdown, 93 - ], 94 - ); 95 - 96 - if (!cardMap) { 25 + if (isLoading) { 97 26 return ( 98 27 <div className="mt-8 pt-8 border-t border-gray-200 dark:border-slate-700"> 99 28 <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> ··· 108 37 ); 109 38 } 110 39 111 - if (cards.length === 0) { 40 + const hasData = 41 + manaCurve.length > 0 || 42 + typeDistribution.length > 0 || 43 + subtypeDistribution.length > 0 || 44 + speedDistribution.length > 0 || 45 + manaBreakdown.length > 0; 46 + 47 + if (!hasData) { 112 48 return null; 113 49 } 114 50 ··· 118 54 Statistics 119 55 </h2> 120 56 121 - <div className="flex gap-6"> 122 - {/* Charts area */} 123 - <div className="flex-1 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> 124 - <ManaCurveChart 125 - data={manaCurve} 126 - selection={selection} 127 - onSelect={setSelection} 128 - /> 129 - <TypesPieChart 130 - data={typeDistribution} 131 - selection={selection} 132 - onSelect={setSelection} 133 - /> 134 - <SpeedPieChart 135 - data={speedDistribution} 136 - selection={selection} 137 - onSelect={setSelection} 138 - /> 139 - <SubtypesPieChart 140 - data={subtypeDistribution} 141 - selection={selection} 142 - onSelect={setSelection} 143 - /> 144 - <ManaBreakdown 145 - data={manaBreakdown} 146 - selection={selection} 147 - onSelect={setSelection} 148 - /> 149 - </div> 150 - 151 - {/* Card list panel */} 152 - {selectedCards.length > 0 && ( 153 - <div className="flex-shrink-0"> 154 - <StatsCardList 155 - title={selectedTitle} 156 - cards={selectedCards} 157 - onCardHover={onCardHover} 158 - onCardClick={onCardClick} 159 - /> 160 - </div> 161 - )} 57 + <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> 58 + <ManaCurveChart 59 + data={manaCurve} 60 + selection={selection} 61 + onSelect={onSelect} 62 + /> 63 + <TypesPieChart 64 + data={typeDistribution} 65 + selection={selection} 66 + onSelect={onSelect} 67 + /> 68 + <SpeedPieChart 69 + data={speedDistribution} 70 + selection={selection} 71 + onSelect={onSelect} 72 + /> 73 + <SubtypesPieChart 74 + data={subtypeDistribution} 75 + selection={selection} 76 + onSelect={onSelect} 77 + /> 78 + <ManaBreakdown 79 + data={manaBreakdown} 80 + selection={selection} 81 + onSelect={onSelect} 82 + /> 162 83 </div> 163 84 </div> 164 85 );
+1 -1
src/components/deck/stats/StatsCardList.tsx
··· 34 34 }); 35 35 36 36 return ( 37 - <div className="bg-gray-50 dark:bg-slate-800 rounded-lg p-4 min-w-48 max-h-80 overflow-y-auto"> 37 + <div className="bg-gray-50 dark:bg-slate-800 rounded-lg p-4 min-w-48"> 38 38 <h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 39 39 {title} 40 40 </h4>
+80
src/lib/useDeckStats.ts
··· 1 + import { useQueries } from "@tanstack/react-query"; 2 + import { useMemo } from "react"; 3 + import { 4 + computeManaCurve, 5 + computeManaSymbolsVsSources, 6 + computeSpeedDistribution, 7 + computeSubtypeDistribution, 8 + computeTypeDistribution, 9 + type ManaCurveData, 10 + type ManaSymbolsData, 11 + type SpeedData, 12 + type TypeData, 13 + } from "@/lib/deck-stats"; 14 + import type { DeckCard } from "@/lib/deck-types"; 15 + import { combineCardQueries, getCardByIdQueryOptions } from "@/lib/queries"; 16 + 17 + export interface DeckStatsData { 18 + manaCurve: ManaCurveData[]; 19 + typeDistribution: TypeData[]; 20 + subtypeDistribution: TypeData[]; 21 + speedDistribution: SpeedData[]; 22 + manaBreakdown: ManaSymbolsData[]; 23 + isLoading: boolean; 24 + } 25 + 26 + export function useDeckStats(cards: DeckCard[]): DeckStatsData { 27 + const cardMap = useQueries({ 28 + queries: cards.map((card) => getCardByIdQueryOptions(card.scryfallId)), 29 + combine: combineCardQueries, 30 + }); 31 + 32 + const manaCurve = useMemo( 33 + () => 34 + cardMap 35 + ? computeManaCurve(cards, (dc) => cardMap.get(dc.scryfallId)) 36 + : [], 37 + [cards, cardMap], 38 + ); 39 + 40 + const typeDistribution = useMemo( 41 + () => 42 + cardMap 43 + ? computeTypeDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 44 + : [], 45 + [cards, cardMap], 46 + ); 47 + 48 + const subtypeDistribution = useMemo( 49 + () => 50 + cardMap 51 + ? computeSubtypeDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 52 + : [], 53 + [cards, cardMap], 54 + ); 55 + 56 + const speedDistribution = useMemo( 57 + () => 58 + cardMap 59 + ? computeSpeedDistribution(cards, (dc) => cardMap.get(dc.scryfallId)) 60 + : [], 61 + [cards, cardMap], 62 + ); 63 + 64 + const manaBreakdown = useMemo( 65 + () => 66 + cardMap 67 + ? computeManaSymbolsVsSources(cards, (dc) => cardMap.get(dc.scryfallId)) 68 + : [], 69 + [cards, cardMap], 70 + ); 71 + 72 + return { 73 + manaCurve, 74 + typeDistribution, 75 + subtypeDistribution, 76 + speedDistribution, 77 + manaBreakdown, 78 + isLoading: !cardMap, 79 + }; 80 + }
+47 -9
src/routes/profile/$did/deck/$rkey/index.tsx
··· 2 2 import { type DragEndEvent, useDndMonitor } from "@dnd-kit/core"; 3 3 import { useSuspenseQuery } from "@tanstack/react-query"; 4 4 import { createFileRoute, Link } from "@tanstack/react-router"; 5 - import { useState } from "react"; 5 + import { useMemo, useState } from "react"; 6 6 import { toast } from "sonner"; 7 7 import { CardDragOverlay } from "@/components/deck/CardDragOverlay"; 8 8 import { CardModal } from "@/components/deck/CardModal"; ··· 15 15 import { DragDropProvider } from "@/components/deck/DragDropProvider"; 16 16 import type { DragData } from "@/components/deck/DraggableCard"; 17 17 import { GoldfishView } from "@/components/deck/GoldfishView"; 18 + import { StatsCardList } from "@/components/deck/stats/StatsCardList"; 18 19 import { TrashDropZone } from "@/components/deck/TrashDropZone"; 19 20 import { ViewControls } from "@/components/deck/ViewControls"; 20 21 import { asRkey } from "@/lib/atproto-client"; ··· 34 35 import { getCardByIdQueryOptions } from "@/lib/queries"; 35 36 import type { ScryfallId } from "@/lib/scryfall-types"; 36 37 import { getImageUri } from "@/lib/scryfall-utils"; 38 + import { getSelectedCards, type StatsSelection } from "@/lib/stats-selection"; 37 39 import { useAuth } from "@/lib/useAuth"; 40 + import { useDeckStats } from "@/lib/useDeckStats"; 38 41 import { usePersistedState } from "@/lib/usePersistedState"; 39 42 40 43 export const Route = createFileRoute("/profile/$did/deck/$rkey/")({ ··· 119 122 const [modalCard, setModalCard] = useState<DeckCard | null>(null); 120 123 const [draggedCardId, setDraggedCardId] = useState<ScryfallId | null>(null); 121 124 const [isDragging, setIsDragging] = useState(false); 125 + const [statsSelection, setStatsSelection] = useState<StatsSelection>(null); 126 + 127 + const statsCards = useMemo( 128 + () => [ 129 + ...getCardsInSection(deck, "commander"), 130 + ...getCardsInSection(deck, "mainboard"), 131 + ], 132 + [deck], 133 + ); 134 + const stats = useDeckStats(statsCards); 135 + const selectedCards = useMemo( 136 + () => getSelectedCards(statsSelection, stats), 137 + [statsSelection, stats], 138 + ); 122 139 123 140 const mutation = useUpdateDeckMutation(did as Did, asRkey(rkey)); 124 141 const queryClient = Route.useRouteContext().queryClient; ··· 363 380 draggedCardId={draggedCardId} 364 381 isDragging={isDragging} 365 382 isOwner={isOwner} 383 + stats={stats} 384 + statsSelection={statsSelection} 385 + selectedCards={selectedCards} 386 + setStatsSelection={setStatsSelection} 366 387 setIsDragging={setIsDragging} 367 388 setDraggedCardId={setDraggedCardId} 368 389 handleCardHover={handleCardHover} ··· 393 414 draggedCardId: ScryfallId | null; 394 415 isDragging: boolean; 395 416 isOwner: boolean; 417 + stats: ReturnType<typeof useDeckStats>; 418 + statsSelection: StatsSelection; 419 + selectedCards: ReturnType<typeof getSelectedCards>; 420 + setStatsSelection: (selection: StatsSelection) => void; 396 421 setIsDragging: (dragging: boolean) => void; 397 422 setDraggedCardId: (id: ScryfallId | null) => void; 398 423 handleCardHover: (cardId: ScryfallId | null) => void; ··· 420 445 draggedCardId, 421 446 isDragging, 422 447 isOwner, 448 + stats, 449 + statsSelection, 450 + selectedCards, 451 + setStatsSelection, 423 452 setIsDragging, 424 453 setDraggedCardId, 425 454 handleCardHover, ··· 503 532 {/* Main content */} 504 533 <div className="max-w-7xl 2xl:max-w-[96rem] mx-auto px-6 py-8"> 505 534 <div className="flex flex-col md:flex-row gap-6"> 506 - {/* Left pane: Card preview (fixed width) */} 535 + {/* Left pane: Card preview + stats card list (fixed width) */} 507 536 <div className="hidden md:block md:w-48 lg:w-60 xl:w-80 md:flex-shrink-0"> 508 - <CardPreviewPane cardId={previewCard} /> 537 + <div className="sticky top-20 max-h-[calc(100vh-6rem)] flex flex-col"> 538 + <CardPreviewPane cardId={previewCard} /> 539 + {selectedCards.cards.length > 0 && ( 540 + <div className="min-h-0 flex-1 overflow-y-auto mt-8"> 541 + <StatsCardList 542 + title={selectedCards.title} 543 + cards={selectedCards.cards} 544 + onCardHover={handleCardHover} 545 + onCardClick={handleCardClick} 546 + /> 547 + </div> 548 + )} 549 + </div> 509 550 </div> 510 551 511 552 {/* Right pane: Deck sections (fills remaining space) */} ··· 559 600 /> 560 601 561 602 <DeckStats 562 - cards={[ 563 - ...getCardsInSection(deck, "commander"), 564 - ...getCardsInSection(deck, "mainboard"), 565 - ]} 566 - onCardHover={handleCardHover} 567 - onCardClick={handleCardClick} 603 + stats={stats} 604 + selection={statsSelection} 605 + onSelect={setStatsSelection} 568 606 /> 569 607 570 608 <GoldfishView