diff --git a/apps/client/src/scenes/result/ResultScene.tsx b/apps/client/src/scenes/result/ResultScene.tsx index acd206e..d52f159 100644 --- a/apps/client/src/scenes/result/ResultScene.tsx +++ b/apps/client/src/scenes/result/ResultScene.tsx @@ -4,16 +4,14 @@ * 背景演出と順位表の切り替え表示を制御する */ import type { GameResultPayload } from "@repo/shared"; -import { useEffect, useState } from "react"; +import { useMemo } from "react"; import { config } from "../../config"; import { ResultActionBar } from "./components/ResultActionBar"; import { ResultBackground } from "./components/ResultBackground"; -import { ResultPlayerRankingTable } from "./components/ResultPlayerRankingTable"; -import { ResultRankingTable } from "./components/ResultRankingTable"; +import { ResultTabContent } from "./components/ResultTabContent"; import { ResultTabBar } from "./components/ResultTabBar"; import { RESULT_BACKGROUND_DARK_OVERLAY_STYLE, - RESULT_CONTENT_FADE_STYLE, RESULT_CONTENT_STYLE, RESULT_KEYFRAMES_CSS, RESULT_ROOT_STYLE, @@ -21,26 +19,22 @@ RESULT_TITLE_STYLE, getResultTitleTextStyle, } from "./styles/resultStyles"; -import type { ResultTabMode } from "./types/resultTabMode"; -import type { ResultViewMode } from "./types/resultViewMode"; +import { useResultView } from "./hooks/useResultView"; type Props = { result: GameResultPayload | null; onBackToTitle: () => void; }; -const formatPaintRate = (value: number): string => `${value.toFixed(1)}%`; - /** 最終結果データを受け取り,順位一覧を表示する */ export const ResultScene = ({ result, onBackToTitle }: Props) => { - const [viewMode, setViewMode] = useState("mapPreview"); - const [activeTab, setActiveTab] = useState("teamRanking"); - const isRankingVisible = viewMode === "ranking"; - - useEffect(() => { - setViewMode("mapPreview"); - setActiveTab("teamRanking"); - }, [result]); + const { + activeTab, + isRankingVisible, + showMapPreview, + showRanking, + setActiveTab, + } = useResultView(result); if (!result) { return ( @@ -48,18 +42,27 @@ ); } - const winnerTeamId = - result.rankings.find((row) => row.rank === 1)?.teamId ?? - result.rankings[0]?.teamId; - const winnerColor = - config.GAME_CONFIG.TEAM_COLORS[winnerTeamId ?? -1] ?? "#888888"; + const winnerTeamId = useMemo( + () => + result.rankings.find((row) => row.rank === 1)?.teamId ?? + result.rankings[0]?.teamId, + [result.rankings], + ); + const winnerColor = useMemo( + () => config.GAME_CONFIG.TEAM_COLORS[winnerTeamId ?? -1] ?? "#888888", + [winnerTeamId], + ); const gridCols = config.GAME_CONFIG.GRID_COLS; const gridRows = config.GAME_CONFIG.GRID_ROWS; const totalCells = gridCols * gridRows; - const finalGridColors = Array.from({ length: totalCells }, (_, index) => { - const teamId = result.finalGridColors?.[index]; - return typeof teamId === "number" ? teamId : -1; - }); + const finalGridColors = useMemo( + () => + Array.from({ length: totalCells }, (_, index) => { + const teamId = result.finalGridColors?.[index]; + return typeof teamId === "number" ? teamId : -1; + }), + [result.finalGridColors, totalCells], + ); return (
@@ -88,7 +91,7 @@ {isRankingVisible && ( setViewMode("mapPreview")} + onShowMapPreview={showMapPreview} /> )} @@ -104,40 +107,13 @@ )} - {isRankingVisible && activeTab === "teamRanking" && ( -
- -
+ {isRankingVisible && ( + )} - - {isRankingVisible && - activeTab === "paintCount" && - result.playerStats && - result.playerStats.length > 0 && ( -
- -
- )} - - {isRankingVisible && - activeTab === "bombHits" && - result.playerStats && - result.playerStats.length > 0 && ( -
- -
- )}
); diff --git a/apps/client/src/scenes/result/components/ResultPlayerRankingTable.tsx b/apps/client/src/scenes/result/components/ResultPlayerRankingTable.tsx index 82e7689..3f6059d 100644 --- a/apps/client/src/scenes/result/components/ResultPlayerRankingTable.tsx +++ b/apps/client/src/scenes/result/components/ResultPlayerRankingTable.tsx @@ -4,6 +4,7 @@ * 塗り回数またはヒット数でソートして順位表示を担当する */ import type { PlayerGameStats } from "@repo/shared"; +import { useMemo } from "react"; import { config } from "@client/config"; import { RESULT_PLAYER_RANKING_HEADER_ROW_STYLE, @@ -29,7 +30,10 @@ sortKey, valueLabel, }: Props) => { - const sorted = [...playerStats].sort((a, b) => b[sortKey] - a[sortKey]); + const sorted = useMemo( + () => [...playerStats].sort((a, b) => b[sortKey] - a[sortKey]), + [playerStats, sortKey], + ); return (
diff --git a/apps/client/src/scenes/result/components/ResultTabContent.tsx b/apps/client/src/scenes/result/components/ResultTabContent.tsx new file mode 100644 index 0000000..6941c67 --- /dev/null +++ b/apps/client/src/scenes/result/components/ResultTabContent.tsx @@ -0,0 +1,61 @@ +/** + * ResultTabContent + * リザルト画面のタブ内容表示を一元化する + */ +import type { GameResultRanking, PlayerGameStats } from "@repo/shared"; +import { ResultPlayerRankingTable } from "./ResultPlayerRankingTable"; +import { ResultRankingTable } from "./ResultRankingTable"; +import { RESULT_CONTENT_FADE_STYLE } from "../styles/resultStyles"; +import type { ResultTabMode } from "../types/resultTabMode"; + +type Props = { + activeTab: ResultTabMode; + rankings: GameResultRanking[]; + playerStats: PlayerGameStats[]; +}; + +const formatPaintRate = (value: number): string => `${value.toFixed(1)}%`; + +/** 選択中タブに応じたコンテンツを描画する */ +export const ResultTabContent = ({ + activeTab, + rankings, + playerStats, +}: Props) => { + if (activeTab === "teamRanking") { + return ( +
+ +
+ ); + } + + if (playerStats.length === 0) { + return null; + } + + if (activeTab === "paintCount") { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/apps/client/src/scenes/result/hooks/useResultView.ts b/apps/client/src/scenes/result/hooks/useResultView.ts new file mode 100644 index 0000000..b6e46b0 --- /dev/null +++ b/apps/client/src/scenes/result/hooks/useResultView.ts @@ -0,0 +1,37 @@ +/** + * useResultView + * リザルト画面の表示モードとタブ状態を管理する + */ +import { useEffect, useState } from "react"; +import type { ResultTabMode } from "../types/resultTabMode"; +import type { ResultViewMode } from "../types/resultViewMode"; + +type UseResultViewReturn = { + viewMode: ResultViewMode; + activeTab: ResultTabMode; + isRankingVisible: boolean; + showRanking: () => void; + showMapPreview: () => void; + setActiveTab: (tab: ResultTabMode) => void; +}; + +/** リザルト画面のビュー状態を返す */ +export const useResultView = (resultToken: unknown): UseResultViewReturn => { + const [viewMode, setViewMode] = useState("mapPreview"); + const [activeTab, setActiveTab] = useState("teamRanking"); + const isRankingVisible = viewMode === "ranking"; + + useEffect(() => { + setViewMode("mapPreview"); + setActiveTab("teamRanking"); + }, [resultToken]); + + return { + viewMode, + activeTab, + isRankingVisible, + showRanking: () => setViewMode("ranking"), + showMapPreview: () => setViewMode("mapPreview"), + setActiveTab, + }; +};