Skip to content

Commit 026038a

Browse files
committed
feat: page titles and further tag improvements
1 parent 080bc97 commit 026038a

23 files changed

Lines changed: 531 additions & 205 deletions

File tree

‎assets/react/src/components/app-shell.tsx‎

Lines changed: 155 additions & 190 deletions
Large diffs are not rendered by default.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useRouterState } from "@tanstack/react-router"
2+
import type { PropsWithChildren } from "react"
3+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
4+
5+
declare module "@tanstack/react-router" {
6+
interface StaticDataRouteOption {
7+
title?: string
8+
}
9+
}
10+
11+
const APP_TITLE = "ManaVault"
12+
13+
type PageTitleState = {
14+
pathname: string
15+
title: string | null | undefined
16+
}
17+
18+
const PageTitleContext = createContext<((title: string | null | undefined) => void) | null>(null)
19+
20+
export function formatPageTitle(title: string | null | undefined) {
21+
const trimmedTitle = title?.trim()
22+
return trimmedTitle ? `${APP_TITLE} - ${trimmedTitle}` : APP_TITLE
23+
}
24+
25+
export function PageTitleProvider({ children }: PropsWithChildren) {
26+
const pathname = useRouterState({ select: (state) => state.location.pathname })
27+
const routeTitle = useRouterState({
28+
select: (state) => {
29+
for (let index = state.matches.length - 1; index >= 0; index -= 1) {
30+
const title = state.matches[index]?.staticData.title
31+
if (typeof title === "string") return title
32+
}
33+
34+
return undefined
35+
},
36+
})
37+
const [pageTitle, setPageTitle] = useState<PageTitleState | null>(null)
38+
const title = pageTitle?.pathname === pathname ? pageTitle.title : routeTitle
39+
const setCurrentPageTitle = useCallback(
40+
(nextTitle: string | null | undefined) => setPageTitle({ pathname, title: nextTitle }),
41+
[pathname],
42+
)
43+
const contextValue = useMemo(() => setCurrentPageTitle, [setCurrentPageTitle])
44+
45+
useEffect(() => {
46+
if (typeof document === "undefined") return
47+
document.title = formatPageTitle(title)
48+
}, [title])
49+
50+
return <PageTitleContext.Provider value={contextValue}>{children}</PageTitleContext.Provider>
51+
}
52+
53+
export function usePageTitle(title: string | null | undefined) {
54+
const setPageTitle = useContext(PageTitleContext)
55+
56+
useEffect(() => {
57+
setPageTitle?.(title)
58+
return () => setPageTitle?.(undefined)
59+
}, [setPageTitle, title])
60+
}

‎assets/react/src/pages/cards/page.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EmptyState } from "../../components/card-image"
66
import { FullscreenPrintingDialog } from "../../components/fullscreen-printing-dialog"
77
import { Button } from "../../components/ui/button"
88
import { graphqlEndpointContext } from "../../lib/apollo"
9+
import { usePageTitle } from "../../lib/page-title"
910
import {
1011
buildCollectionFilterQuery,
1112
cloneCollectionFilters,
@@ -311,6 +312,7 @@ export function CardDetailPage({
311312
const card = data?.card
312313
const visiblePrintings = connectionNodes(card?.printings)
313314
const primary = visiblePrintings[0]
315+
usePageTitle(card?.name ?? (isLoading ? "Card" : "Card not found"))
314316
const previewPrintings = visiblePrintings.map((printing) => ({
315317
...printing,
316318
scryfallId: printing.id,

‎assets/react/src/pages/collection/location-page.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type CollectionFilterState,
2020
} from "../../lib/collection-filters"
2121
import { useLocalStorageState } from "../../lib/use-local-storage"
22+
import { usePageTitle } from "../../lib/page-title"
2223
import { cn, compactNumber, pluralize, present, titleize } from "../../lib/utils"
2324
import { AutoSortSetupDialog, hasEnabledAutoSortRules } from "./auto-sort-setup-dialog"
2425
import { AutoSortSummaryDialog } from "./auto-sort-summary-dialog"
@@ -191,6 +192,7 @@ export function LocationPage({ id }: { id: string }) {
191192
itemsPageInfo?.endCursor,
192193
])
193194
const location = data?.location
195+
usePageTitle(location?.name ?? (isLoading ? "Collection Location" : "Location not found"))
194196
const activeStructuredFilterCount = countActiveCollectionFilters(structuredFilters)
195197
const hasLocationFilters = Boolean(combinedCollectionQuery)
196198
const locationCountLabel = `${countQuery.data?.collectionItemCount ?? location?.itemCount ?? 0} ${hasLocationFilters ? "shown" : "total"}`

‎assets/react/src/pages/decks/deck-readiness.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export type DeckReadinessSummary = {
1010
}
1111

1212
export function deckPullZones(deckCards: readonly DeckCardEntry[]) {
13-
return deckCards.filter((deckCard) => deckCard.zone === "mainboard" || deckCard.zone === "commander")
13+
return deckCards.filter(
14+
(deckCard) => deckCard.zone === "mainboard" || deckCard.zone === "commander",
15+
)
1416
}
1517

1618
export function summarizeDeckPullNeeds(deckCards: readonly DeckCardEntry[]) {

‎assets/react/src/pages/decks/deck-stack-card.tsx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ export function DeckStackCard({
208208
className={cn(
209209
"dropdown dropdown-start absolute left-2 top-2 z-[120] transition-opacity group-focus-within:opacity-100",
210210
actionMenuDirection === "up" && "dropdown-top",
211-
isInteractive ? "visible opacity-100" : "invisible opacity-0 group-hover:visible group-hover:opacity-100",
211+
isInteractive
212+
? "visible opacity-100"
213+
: "invisible opacity-0 group-hover:visible group-hover:opacity-100",
212214
)}
213215
onClick={(event) => event.stopPropagation()}
214216
onMouseDown={(event) => event.stopPropagation()}
@@ -459,15 +461,19 @@ function DeckCardAllocationQuickMenu({
459461
)
460462
const hasProxyAllocation = status.proxyAllocated > 0
461463
const canMarkProxy =
462-
status.state !== "basic_land" && status.proxyAllocated <= 0 && status.required > status.allocated
464+
status.state !== "basic_land" &&
465+
status.proxyAllocated <= 0 &&
466+
status.required > status.allocated
463467

464468
return (
465469
<div className="dropdown dropdown-end" onClick={(event) => event.stopPropagation()}>
466470
<CardTileOverlayButton
467471
className={cn(
468472
"relative transition-opacity",
469473
allocationStatusButtonClass(status.state),
470-
isVisible ? "visible opacity-100" : "invisible opacity-0 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100",
474+
isVisible
475+
? "visible opacity-100"
476+
: "invisible opacity-0 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100",
471477
)}
472478
tabIndex={isVisible ? 0 : -1}
473479
aria-label={`${label}: ${summary}`}
@@ -553,7 +559,9 @@ function DeckCardTagQuickButton({
553559
className={cn(
554560
"transition-opacity",
555561
tag?.className,
556-
isVisible ? "visible opacity-100" : "invisible opacity-0 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100",
562+
isVisible
563+
? "visible opacity-100"
564+
: "invisible opacity-0 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100",
557565
)}
558566
disabled={disabled}
559567
tabIndex={isVisible ? 0 : -1}

‎assets/react/src/pages/decks/detail-page-content.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import type { DeckCardUpdateInput } from "../../gql/graphql"
2929
import type { DeckGroup, DeckGroupBy } from "../../lib/deck-grouping"
3030
import { cn, compactNumber, titleize } from "../../lib/utils"
3131
import { ShareModeHidden, SummaryActionMenu } from "./deck-actions"
32-
import { DeckCardAllocationPanel, allocationStatusLabel, allocationStatusSummary } from "./deck-card-allocation"
32+
import {
33+
DeckCardAllocationPanel,
34+
allocationStatusLabel,
35+
allocationStatusSummary,
36+
} from "./deck-card-allocation"
3337
import { cardImageUrl, deckDetailCoverUrl } from "./deck-card-model"
3438
import { DeckGroupMenu } from "./deck-group-menu"
3539
import { deckLegalityIssueCountLabel, deckLegalityLabel, deckLegalityTone } from "./deck-legality"

‎assets/react/src/pages/decks/detail-page.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useToast } from "../../components/ui/toast"
77
import type { DeckCardInput, DeckCardUpdateInput, DeckQuery } from "../../gql/graphql"
88
import { groupDeckCards, type DeckGroupBy } from "../../lib/deck-grouping"
99
import { graphqlEndpointContext, refetchActiveQueries } from "../../lib/apollo"
10+
import { usePageTitle } from "../../lib/page-title"
1011
import { pluralize } from "../../lib/utils"
1112
import { deckCardsTotalPrice, deckMissingCardsTotalPrice, formatUsdCents } from "./buylist-export"
1213
import {
@@ -230,6 +231,7 @@ export function DeckDetailPage({
230231
const deck = useMemo(() => flattenDeck(deckQueryData?.deck), [deckQueryData?.deck])
231232
const isInitialDeckLoading = isLoading && !deck
232233
const isRefreshingDeck = isLoading && Boolean(deck)
234+
usePageTitle(deck?.name ?? (isInitialDeckLoading ? "Deck" : "Deck not found"))
233235
const [isAddCardOpen, setIsAddCardOpen] = useState(false)
234236
const deckCards = useMemo(() => deck?.deckCards || [], [deck?.deckCards])
235237
const bulkAllocationPullList = useMemo(

‎assets/react/src/pages/decks/playtest-page.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMemo } from "react"
33
import { EmptyState } from "../../components/card-image"
44
import { DeckPlaytester } from "../../components/deck-playtester"
55
import { createPlaytestState } from "../../lib/deck-playtest"
6+
import { usePageTitle } from "../../lib/page-title"
67
import { deckPlaytestCards } from "./deck-card-model"
78
import { flattenDeck } from "./deck-types"
89
import { DeckDocument } from "./queries"
@@ -18,6 +19,7 @@ export function DeckPlaytestPage({ id }: { id: string }) {
1819
() => createPlaytestState(playtestCards.library, playtestCards.command),
1920
[playtestCards],
2021
)
22+
usePageTitle(deck?.name ? `${deck.name} Playtest` : isLoading ? "Playtest" : "Deck not found")
2123

2224
if (isLoading) return <EmptyState title="Loading playtest..." />
2325
if (!deck) return <EmptyState title="Deck not found" />

‎assets/react/src/routes/cards/$id.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type CardSearch = {
1717
const EDHREC_TABS: CardReturnEdhrecTab[] = ["recs", "cuts", "commander"]
1818

1919
export const Route = createFileRoute("/cards/$id")({
20+
staticData: { title: "Card" },
2021
validateSearch: (search: Record<string, unknown>): CardSearch => {
2122
const returnLocationId =
2223
typeof search.returnLocationId === "string" && !search.returnLocationId.includes("/")

0 commit comments

Comments
 (0)