👁️
5
fork

Configure Feed

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

click tag to see the tag in the deck

+90 -16
+2 -1
src/components/deck/DroppableTagGroup.tsx
··· 34 34 return ( 35 35 <div 36 36 ref={setNodeRef} 37 - className="mb-4 break-inside-avoid rounded-lg relative" 37 + data-tag-group={tagName} 38 + className="mb-4 break-inside-avoid rounded-lg relative scroll-mt-20" 38 39 style={{ breakInside: "avoid" }} 39 40 > 40 41 {/* Drop zone visual */}
+16 -5
src/components/richtext/RichtextRenderer.tsx
··· 2 2 import { Link } from "@tanstack/react-router"; 3 3 import { memo, type ReactNode } from "react"; 4 4 import { useCardHover } from "@/components/HoverCardPreview"; 5 + import { useTagClick } from "@/components/richtext/TagClickContext"; 5 6 import type { 6 7 BulletListBlock, 7 8 CodeBlock, ··· 229 230 ); 230 231 } 231 232 233 + function TagPill({ tag, children }: { tag: string; children: ReactNode }) { 234 + const { onTagClick } = useTagClick(); 235 + 236 + return ( 237 + <button 238 + type="button" 239 + onClick={onTagClick ? () => onTagClick(tag) : undefined} 240 + className={`inline-flex items-center px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-sm font-medium${onTagClick ? " cursor-pointer hover:bg-amber-200 dark:hover:bg-amber-900/50 motion-safe:transition-colors" : ""}`} 241 + > 242 + {children} 243 + </button> 244 + ); 245 + } 246 + 232 247 function applyFeature( 233 248 content: ReactNode, 234 249 feature: Facet["features"][number], ··· 282 297 } 283 298 284 299 case "com.deckbelcher.richtext.facet#tag": 285 - return ( 286 - <span className="inline-flex items-center px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-sm font-medium"> 287 - {content} 288 - </span> 289 - ); 300 + return <TagPill tag={feature.tag}>{content}</TagPill>; 290 301 291 302 default: 292 303 return content;
+13
src/components/richtext/TagClickContext.tsx
··· 1 + import { createContext, useContext } from "react"; 2 + 3 + interface TagClickContextValue { 4 + onTagClick?: (tag: string) => void; 5 + } 6 + 7 + const TagClickContext = createContext<TagClickContextValue>({}); 8 + 9 + export function useTagClick() { 10 + return useContext(TagClickContext); 11 + } 12 + 13 + export { TagClickContext };
+59 -10
src/routes/profile/$did/deck/$rkey/index.tsx
··· 22 22 import { ValidationBadge } from "@/components/deck/ValidationBadge"; 23 23 import { ViewControls } from "@/components/deck/ViewControls"; 24 24 import { RichtextSection } from "@/components/richtext/RichtextSection"; 25 + import { TagClickContext } from "@/components/richtext/TagClickContext"; 25 26 import { SocialStats } from "@/components/social/SocialStats"; 26 27 import { useDeckValidation } from "@/hooks/useDeckValidation"; 27 28 import { asRkey } from "@/lib/atproto-client"; ··· 209 210 await mutation.mutateAsync(updated); 210 211 }; 211 212 212 - // Highlight cards that were changed - clear after render so it can trigger again 213 + // Highlight cards that were changed - clear after paint so it can trigger again 213 214 const handleCardsChanged = (changedIds: Set<ScryfallId>) => { 214 215 setHighlightedCards(changedIds); 215 - setTimeout(() => setHighlightedCards(new Set()), 0); 216 + requestAnimationFrame(() => setHighlightedCards(new Set())); 216 217 }; 217 218 218 219 const handleCardHover = (cardId: ScryfallId | null) => { ··· 583 584 }, 584 585 }); 585 586 587 + const scrollToTagGroup = useCallback( 588 + (tag: string) => { 589 + const el = document.querySelector( 590 + `[data-tag-group="${CSS.escape(tag)}"]`, 591 + ); 592 + if (!el) { 593 + toast.info(`No cards have the "${tag}" tag`); 594 + return; 595 + } 596 + 597 + el.scrollIntoView({ behavior: "smooth", block: "start" }); 598 + 599 + // Highlight cards with this tag after scroll finishes 600 + const cardsWithTag = deck.cards.filter((c) => c.tags?.includes(tag)); 601 + if (cardsWithTag.length > 0) { 602 + setTimeout(() => { 603 + handleCardsChanged(new Set(cardsWithTag.map((c) => c.scryfallId))); 604 + }, 400); 605 + } 606 + }, 607 + [deck.cards, handleCardsChanged], 608 + ); 609 + 610 + const handleTagClick = useCallback( 611 + (tag: string) => { 612 + if (groupBy !== "typeAndTags") { 613 + toast.info("Switch to 'Type + Tags' view to see tag groups", { 614 + action: { 615 + label: "Switch", 616 + onClick: () => { 617 + setGroupBy("typeAndTags"); 618 + // Wait for React to render new grouping, then scroll 619 + requestAnimationFrame(() => { 620 + requestAnimationFrame(() => scrollToTagGroup(tag)); 621 + }); 622 + }, 623 + }, 624 + }); 625 + return; 626 + } 627 + 628 + scrollToTagGroup(tag); 629 + }, 630 + [groupBy, setGroupBy, scrollToTagGroup], 631 + ); 632 + 586 633 return ( 587 634 <div className="min-h-screen bg-white dark:bg-slate-900"> 588 635 {/* Deck name and format */} ··· 595 642 readOnly={!isOwner} 596 643 /> 597 644 <ErrorBoundary fallback={null}> 598 - <RichtextSection 599 - document={primer} 600 - onSave={onPrimerSave} 601 - isSaving={isSaving} 602 - readOnly={!isOwner} 603 - placeholder="Write about your deck's strategy, key combos, card choices..." 604 - availableTags={allTags} 605 - /> 645 + <TagClickContext.Provider value={{ onTagClick: handleTagClick }}> 646 + <RichtextSection 647 + document={primer} 648 + onSave={onPrimerSave} 649 + isSaving={isSaving} 650 + readOnly={!isOwner} 651 + placeholder="Write about your deck's strategy, key combos, card choices..." 652 + availableTags={allTags} 653 + /> 654 + </TagClickContext.Provider> 606 655 </ErrorBoundary> 607 656 </div> 608 657