From 102e7cac1eccf06fc0d551e8583885faa2ca5fcf Mon Sep 17 00:00:00 2001 From: Josh Payette Date: Mon, 18 May 2026 21:18:43 -0400 Subject: [PATCH] Various fixes for antipatterns and other issues identified by react-doctor --- CLAUDE.md | 8 ++-- .../components/GettingStartedWizard.tsx | 33 ++++++++-------- .../getting-started/constants/steps.tsx | 2 +- src/features/auth/core/AvatarPicker.tsx | 38 +++++++++++-------- src/features/game/core/GameSwitcher.tsx | 5 ++- src/features/game/core/types.ts | 4 +- src/features/game/items/ItemInfoModal.tsx | 5 ++- src/features/game/registry/game-registry.tsx | 14 +++---- .../screenshot/core/ScreenshotContainer.tsx | 6 +-- .../screenshot/core/ScreenshotWatermark.tsx | 8 ++-- src/features/wizard/Wizard.tsx | 4 +- .../clairobscur/core/game-config/metadata.tsx | 3 +- .../remnant2/core/game-config/metadata.tsx | 3 +- .../core/game-config/metadata.tsx | 3 +- src/routes/__root.tsx | 2 +- 15 files changed, 73 insertions(+), 65 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9a3de39..6202fdd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,7 +94,7 @@ Everything game-specific hangs off a central registry. Each game under `src/game const GAME_CONFIG = { ITEMS, // { all, collectable, categorized, categories, uncollectableCategories } THEME, // ToolkitThemeDefinition | undefined - METADATA, // id, name, label, description, faviconSourcePath, renderLogo(), externalResources[] + METADATA, // id, name, label, description, faviconSourcePath, LogoComponent, externalResources[] PAGES, // { renderItemLookup, renderCollectedItems } SEARCH_PARAMS, // nuqs search param cache | undefined (when the game has no custom filters) AVATARS, // GameAvatar[] (optional) @@ -103,7 +103,7 @@ const GAME_CONFIG = { export { GAME_CONFIG }; ``` -`src/features/game/registry/game-registry.tsx` wires every `gameId` to its `GameConfig` and exports: `GAME_REGISTRY`, `REGISTERED_GAME_IDS`, `getGameConfig()`, `getGameConfigTyped()`, `getGameItems()`, `getGameTheme()`, `getGameMetadata()`, `getGamePages()`, `getGameAvatars()`, `getGameLogo()`, `getGameSearchParams()`, `getAllRegisteredThemeDefinitions()`, `getAllRegisteredThemeClassNames()`, `isRegisteredGameId()`, `getValidatedGameId()`. +`src/features/game/registry/game-registry.tsx` wires every `gameId` to its `GameConfig` and exports: `GAME_REGISTRY`, `REGISTERED_GAME_IDS`, `getGameConfig()`, `getGameConfigTyped()`, `getGameItems()`, `getGameTheme()`, `getGameMetadata()`, `getGamePages()`, `getGameAvatars()`, `getGameLogoComponent()`, `getGameSearchParams()`, `getAllRegisteredThemeDefinitions()`, `getAllRegisteredThemeClassNames()`, `isRegisteredGameId()`, `getValidatedGameId()`. The `GameId` enum is defined in `schema.prisma` and imported from `@/prisma`. `getAllRegisteredThemeDefinitions()` expands each game theme into light+dark variants plus a base `default-light`/`default-dark`. @@ -140,7 +140,7 @@ Follow these steps in order. The registry is the single place to check; no other core/ game-config/ index.ts # exports GAME_CONFIG satisfies GameConfig - metadata.tsx # id, name, label, description, faviconSourcePath, renderLogo() + metadata.tsx # id, name, label, description, faviconSourcePath, LogoComponent pages.tsx # GamePages with renderItemLookup() and renderCollectedItems() theme.ts # ToolkitThemeDefinition (colors, Mantine overrides) — uses generateThemeColors() from #/features/theme/core/generate-palette items.ts # item data + categorization @@ -151,7 +151,7 @@ Follow these steps in order. The registry is the single place to check; no other item-data/ # raw item definitions consumed by game-config/items.ts types.ts # game-specific TypeScript types (LocalItem, etc.) constants.ts # game-specific constants - Logo.tsx # game logo component referenced by metadata.renderLogo() + Logo.tsx # game logo component referenced by metadata.LogoComponent dal/ collected-items.ts # exports the GameCollectedItemsDal via createCollectedItemsDal() server/ diff --git a/src/components/wizards/getting-started/components/GettingStartedWizard.tsx b/src/components/wizards/getting-started/components/GettingStartedWizard.tsx index 0a4b959..8e255e1 100644 --- a/src/components/wizards/getting-started/components/GettingStartedWizard.tsx +++ b/src/components/wizards/getting-started/components/GettingStartedWizard.tsx @@ -24,23 +24,26 @@ const GettingStartedWizard = ({ const isMobile = useMediaQuery("(max-width: 768px)"); - const adaptedSteps = GETTING_STARTED_STEPS - // On mobile, remove targetSelector for social-media step since - // the footer is not visible with the drawer open - .map((step) => { - if (isMobile && step.id === "social-media") { - return { ...step, targetSelector: undefined }; - } - return step; - }) - // if no gameId, remove the favorite-game slide since the - // favorite game heart icon is not visible - .filter((step) => { + const adaptedSteps = GETTING_STARTED_STEPS.reduce( + (acc, step) => { + // if no gameId, remove the favorite-game slide since the + // favorite game heart icon is not visible if (step.id === "favorite-game" && !gameId) { - return false; + return acc; } - return true; - }); + + // On mobile, remove targetSelector for social-media step since + // the footer is not visible with the drawer open + if (isMobile && step.id === "social-media") { + acc.push({ ...step, targetSelector: undefined }); + } else { + acc.push(step); + } + + return acc; + }, + [], + ); const handleBeforeOpen = () => { if (isMobile && !navbarOpened) { diff --git a/src/components/wizards/getting-started/constants/steps.tsx b/src/components/wizards/getting-started/constants/steps.tsx index 810903b..c5a91d2 100644 --- a/src/components/wizards/getting-started/constants/steps.tsx +++ b/src/components/wizards/getting-started/constants/steps.tsx @@ -67,7 +67,7 @@ const GETTING_STARTED_STEPS: WizardStep[] = [ GitHub . A major goal is to help people make their first open-source - contributions—check out the repo or message me on Discord to get + contributions-check out the repo or message me on Discord to get involved! diff --git a/src/features/auth/core/AvatarPicker.tsx b/src/features/auth/core/AvatarPicker.tsx index e59e0f6..f3d438b 100644 --- a/src/features/auth/core/AvatarPicker.tsx +++ b/src/features/auth/core/AvatarPicker.tsx @@ -27,7 +27,7 @@ import { useGameId } from "#/features/game/core/use-game-id"; import { getGameAvatars, getGameConfig, - getGameLogo, + getGameLogoComponent, REGISTERED_GAME_IDS, } from "#/features/game/registry/game-registry"; import type { GameId } from "@/prisma"; @@ -101,6 +101,9 @@ export function AvatarPicker() { const browsingGame = gamesWithAvatars.find( (g) => g.gameId === browsingGameId, ); + const BrowsingGameLogo = browsingGame + ? getGameLogoComponent(browsingGame.gameId) + : undefined; const overrideForBrowsedGame = avatarOverrides.find( (o) => o.gameId === browsingGameId, ); @@ -278,20 +281,23 @@ export function AvatarPicker() { ); }; - const renderGameRow = (game: GameWithAvatars, compact: boolean) => ( - handleGameChange(game.gameId)} - > - - {getGameLogo(game.gameId, compact ? 24 : 36)} - - {game.label} - - - - ); + const renderGameRow = (game: GameWithAvatars, compact: boolean) => { + const LogoComponent = getGameLogoComponent(game.gameId); + return ( + handleGameChange(game.gameId)} + > + + {LogoComponent && } + + {game.label} + + + + ); + }; return ( @@ -385,7 +391,7 @@ export function AvatarPicker() { > - {browsingGame && getGameLogo(browsingGame.gameId, 36)} + {BrowsingGameLogo && } {browsingGame?.label ?? "Select a game"} diff --git a/src/features/game/core/GameSwitcher.tsx b/src/features/game/core/GameSwitcher.tsx index 2bcc82e..c114c8f 100644 --- a/src/features/game/core/GameSwitcher.tsx +++ b/src/features/game/core/GameSwitcher.tsx @@ -30,7 +30,7 @@ import { useGameId } from "#/features/game/core/use-game-id"; import { setActiveGameCookie } from "#/features/game/core/utils"; import { getGameConfig, - getGameLogo, + getGameLogoComponent, REGISTERED_GAME_IDS, } from "#/features/game/registry/game-registry"; import type { GameId } from "@/prisma"; @@ -110,6 +110,7 @@ function GameSwitcher() { const activeLabel = getGameConfig(activeGameId)?.THEME?.label ?? "Toolkits.gg"; + const ActiveGameLogo = getGameLogoComponent(activeGameId); const filteredGames = allGames.filter((g) => g.label.toLowerCase().includes(searchQuery.toLowerCase()), @@ -188,7 +189,7 @@ function GameSwitcher() { > - {getGameLogo(activeGameId) || } + {ActiveGameLogo ? : } {activeLabel} diff --git a/src/features/game/core/types.ts b/src/features/game/core/types.ts index 6b72349..5131733 100644 --- a/src/features/game/core/types.ts +++ b/src/features/game/core/types.ts @@ -1,5 +1,5 @@ import type { createSearchParamsCache } from "nuqs/server"; -import type { ReactNode } from "react"; +import type { ComponentType, ReactNode } from "react"; import type { LogoSize } from "#/components/AppLogo"; import type { AppItem, @@ -37,7 +37,7 @@ type GameMetadata = { description: string; /** CloudFront-relative path to the source PNG used for favicon generation */ faviconSourcePath: string; - renderLogo: (size: LogoSize) => ReactNode; + LogoComponent: ComponentType<{ size?: LogoSize }>; /** Third-party resources related to the game */ externalResources: { label: string; diff --git a/src/features/game/items/ItemInfoModal.tsx b/src/features/game/items/ItemInfoModal.tsx index 3629c1a..5e40266 100644 --- a/src/features/game/items/ItemInfoModal.tsx +++ b/src/features/game/items/ItemInfoModal.tsx @@ -51,7 +51,10 @@ const ItemInfoModal = ({ const watermark: WatermarkConfig | false = metadata ? { gameConfig: { - METADATA: { renderLogo: metadata.renderLogo, label: metadata.label }, + METADATA: { + LogoComponent: metadata.LogoComponent, + label: metadata.label, + }, }, } : false; diff --git a/src/features/game/registry/game-registry.tsx b/src/features/game/registry/game-registry.tsx index 87dde90..fac96df 100644 --- a/src/features/game/registry/game-registry.tsx +++ b/src/features/game/registry/game-registry.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import type { ComponentType } from "react"; import type { LogoSize } from "#/components/AppLogo"; import type { GameAvatar, GameConfig } from "#/features/game/core/types"; import type { ToolkitThemeDefinition } from "#/features/theme/core/types"; @@ -45,11 +45,11 @@ function getGameItems(gameId: string): AnyGameConfig["ITEMS"] | undefined { | undefined; } -// Logo shortcut - returns METADATA.renderLogo response or undefined -function getGameLogo(gameId: string, logoSize: LogoSize = 36): ReactNode { - return GAME_REGISTRY[gameId as RegistryGameId]?.METADATA?.renderLogo?.( - logoSize, - ); +// Logo shortcut - returns METADATA.LogoComponent or undefined +function getGameLogoComponent( + gameId: string, +): ComponentType<{ size?: LogoSize }> | undefined { + return GAME_REGISTRY[gameId as RegistryGameId]?.METADATA?.LogoComponent; } // Search params shortcut - returns SEARCH_PARAMS or undefined @@ -137,7 +137,7 @@ export { getAllRegisteredThemeDefinitions, getGameAvatars, getGameConfig, - getGameLogo, + getGameLogoComponent, getGameItems, getGameMetadata, getGamePages, diff --git a/src/features/screenshot/core/ScreenshotContainer.tsx b/src/features/screenshot/core/ScreenshotContainer.tsx index e4ff889..eb309b7 100644 --- a/src/features/screenshot/core/ScreenshotContainer.tsx +++ b/src/features/screenshot/core/ScreenshotContainer.tsx @@ -1,6 +1,6 @@ import { Avatar, Box, type BoxProps, Group, Stack, Text } from "@mantine/core"; import cx from "clsx"; -import type { ReactNode } from "react"; +import type { ComponentType, ReactNode } from "react"; import { forwardRef } from "react"; import type { LogoSize } from "#/components/AppLogo"; import { ScreenshotWatermark } from "#/features/screenshot/core/ScreenshotWatermark"; @@ -8,7 +8,7 @@ import classes from "./ScreenshotContainer.module.css"; type WatermarkGameConfig = { METADATA: { - renderLogo: (size: LogoSize) => ReactNode; + LogoComponent: ComponentType<{ size?: LogoSize }>; label: string; }; }; @@ -90,7 +90,7 @@ const ScreenshotContainer = forwardRef< {showWatermark && ( ReactNode; + LogoComponent: ComponentType<{ size?: LogoSize }>; logoSize?: LogoSize; label: string; fontSize?: string; @@ -12,7 +12,7 @@ type ScreenshotWatermarkProps = { }; const ScreenshotWatermark = ({ - renderLogo, + LogoComponent, logoSize = DEFAULT_LOGO_SIZE, label, fontSize = "md", @@ -20,7 +20,7 @@ const ScreenshotWatermark = ({ }: ScreenshotWatermarkProps) => { return ( - {renderLogo(logoSize)} + toolkits.gg diff --git a/src/features/wizard/Wizard.tsx b/src/features/wizard/Wizard.tsx index 7eb01e7..7a0826a 100644 --- a/src/features/wizard/Wizard.tsx +++ b/src/features/wizard/Wizard.tsx @@ -83,9 +83,7 @@ const Wizard = ({ if (!opened) return; - openWizard().then(() => { - // placeholder - no action needed - }); + void openWizard(); }, [opened, onBeforeOpen]); useEffect(() => { diff --git a/src/games/clairobscur/core/game-config/metadata.tsx b/src/games/clairobscur/core/game-config/metadata.tsx index 3c3b2af..5777d07 100644 --- a/src/games/clairobscur/core/game-config/metadata.tsx +++ b/src/games/clairobscur/core/game-config/metadata.tsx @@ -1,4 +1,3 @@ -import type { LogoSize } from "#/components/AppLogo"; import type { GameMetadata } from "#/features/game/core/types"; import { GAME_ID } from "#/games/clairobscur/core/constants"; import { ClairObscurLogo } from "#/games/clairobscur/core/Logo"; @@ -9,7 +8,7 @@ const METADATA: GameMetadata = { label: "Clair Obscur", description: `Clair Obscur: Expedition 33 is a turn-based role-playing video game developed by French studio Sandfall Interactive and published by Kepler Interactive. It follows the volunteers of Expedition 33, who set out to destroy the Paintress, a being at the root of the yearly Gommage, which erases those above an ever-decreasing age.`, faviconSourcePath: "games/clairobscur/logos/512C33.png", - renderLogo: (size: LogoSize) => , + LogoComponent: ClairObscurLogo, externalResources: [ { label: "Fandom Wiki", diff --git a/src/games/remnant2/core/game-config/metadata.tsx b/src/games/remnant2/core/game-config/metadata.tsx index 98f5851..a1bde43 100644 --- a/src/games/remnant2/core/game-config/metadata.tsx +++ b/src/games/remnant2/core/game-config/metadata.tsx @@ -1,4 +1,3 @@ -import type { LogoSize } from "#/components/AppLogo"; import type { GameMetadata } from "#/features/game/core/types"; import { GAME_ID } from "#/games/remnant2/core/constants"; import { Remnant2Logo } from "#/games/remnant2/core/Logo"; @@ -9,7 +8,7 @@ const METADATA: GameMetadata = { label: "Remnant 2", description: `REMNANT II® pits survivors of humanity against new deadly creatures and god-like bosses across terrifying worlds. Play solo or co-op with two other friends to explore the depths of the unknown to stop an evil from destroying reality itself.`, faviconSourcePath: "games/remnant2/logos/512R2.png", - renderLogo: (size: LogoSize) => , + LogoComponent: Remnant2Logo, externalResources: [ { label: "Wiki.gg", diff --git a/src/games/slaythespire2/core/game-config/metadata.tsx b/src/games/slaythespire2/core/game-config/metadata.tsx index 57df6f9..bf722cb 100644 --- a/src/games/slaythespire2/core/game-config/metadata.tsx +++ b/src/games/slaythespire2/core/game-config/metadata.tsx @@ -1,4 +1,3 @@ -import type { LogoSize } from "#/components/AppLogo"; import type { GameMetadata } from "#/features/game/core/types"; import { GAME_ID } from "#/games/slaythespire2/core/constants"; import { SlayTheSpire2Logo } from "#/games/slaythespire2/core/Logo"; @@ -9,7 +8,7 @@ const METADATA: GameMetadata = { label: "Slay the Spire 2", description: `The iconic roguelike deckbuilder returns. Craft a unique deck, encounter bizarre creatures, and discover relics of immense power in Slay the Spire 2!`, faviconSourcePath: "games/slaythespire2/logos/512STS2.png", - renderLogo: (size: LogoSize) => , + LogoComponent: SlayTheSpire2Logo, externalResources: [ { label: "Wiki.gg", diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 28d6f2f..b1deb89 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -53,7 +53,7 @@ const url = "https://toolkits.gg"; const Route = createRootRouteWithContext()({ beforeLoad: async ({ context, location }) => { - // Cache the server-fn result for the lifetime of the session — the Host + // Cache the server-fn result for the lifetime of the session - the Host // header and the active-game cookie don't change without a hard reload. const { subdomainGameId, cookieGameId } = await context.queryClient.ensureQueryData({ -- 2.51.2