Skip to content

Design: 候補者詳細画面

要件は requirements.md、ビジュアルは ui-design.md 参照。


アーキテクチャ

app/candidate/[id]/page.tsx (Server)
  ├─ generateStaticParams() で候補者IDを事前生成
  ├─ generateMetadata() で OGP・タイトル動的生成
  ├─ candidateService.getById(id) → notFound() if missing
  └─ <CandidateDetailClient> へ props 渡し

       ├─ useVoteAction(candidateId)
       ├─ useVoteFlow()
       └─ render:
            ├─ <DetailHeader candidate rank />
            ├─ <CandidateImageSlider images />
            ├─ <CandidateInfo candidate />
            ├─ <VoterRanking voters />
            ├─ <SocialLoginModal />
            ├─ <PurchaseCreditsModal />
            └─ <Confetti />

レイヤー別の責務

Server Component: app/candidate/[id]/page.tsx

  • generateStaticParams(): 全候補者IDを返却(SSG)
  • generateMetadata({ params }): 候補者名・OGP画像を含む動的メタデータ
  • candidateService.getById(id): Server側で取得、null なら notFound()
  • 投票者データ: voterService.getByCandidateId(id)

Client Component: app/candidate/[id]/_components/CandidateDetailClient.tsx

  • 投票フロー(useVoteFlow)と投票アクション(useVoteAction)の組み合わせ
  • 画像スライダーの状態管理(currentSlide)

使用フック

フック役割
useVoteAction(candidateId)投票実行(home画面と共通)
useVoteFlow()モーダル・Confetti制御(home画面と共通)
useCandidateRank(candidateId, allCandidates)詳細ページに表示する順位
useImageSlider(images)currentSlide状態 + 切替メソッド

使用コンポーネント

コンポーネントProps
<NavLinks>links: { href, label, icon }[]
<DetailHeader>name, rank, votes
<VoteButtonGroup>freeVoteState, onFreeVote, onPaidVote
<CandidateImageSlider>images, name, currentSlide, onSlideChange
<InfoBadgeRow>items: { label, value }[]
<TagList>title, tags: string[]
<PRMessage>message
<VoterRanking>voters: Voter[], topN: 5

データフロー

詳細ページの順位表示は クライアント側でリアルタイム計算しない。Server側で全候補者の現時点の votes を取得し、ソート → 該当候補者の順位を計算してから渡す。

ts
// Server
const allCandidates = await candidateService.getAll();
const ranked = [...allCandidates].sort((a, b) => b.votes - a.votes);
const rank = ranked.findIndex(c => c.id === id) + 1;

投票後はクライアント側で楽観的に votes を更新するが、ページ全体の順位は再計算しない(Home画面・Ranking画面に戻ったタイミングで反映)。

エッジケース

シナリオ対応
存在しないIDnotFound()not-found.tsx
画像URL切れ<ImageWithFallback> でデフォルト画像表示
voter データなし「まだ応援者がいません」表示(または空)
候補者の images[] が1件のみサムネイル非表示、矢印無効化

SSG/ISR戦略

  • ビルド時: 全9名の候補者ページを静的生成
  • 投票数が変動するため、revalidate: 60 (秒) で1分毎再生成
  • 投票完了時はクライアント側で楽観的に表示し、ISR反映を待たない

関連

  • requirements.md / ui-design.md
  • ../home/design.md — 共通フック・モーダルの詳細
  • ../voting-flow/design.md — 投票ロジック詳細