🫠

ポケモン図鑑:ポケモンの説明文編

2024/01/19に公開

ポケモン図鑑を作る途中で、最低限の情報を表示するより、PokéAPIにおけるできるだけたくさんのデータを画面に表示すればさらなる勉強になると考えた。この記事でDBスキーマの拡張とAPIから自分のDBへのデータ移動をどうやって行なったか説明する。

更新前の状態までの開発にご興味のある方は次の記事をご覧ください。

更新前の状態:

かっこいい詳細メニュー

目標:hybridshivamさんのポケモン図鑑をご参照いただければと存じます。

更新前のスキーマ

更新前はDBポケモンの基本情報しか保管していなかった(フィールド名はポケモン王国攻略館を参考にしている):

  • 名前
  • 分類
  • 全国図鑑の番号
  • 高さ
  • 重さ
  • 特性と隠れ特性
  • タイプ
  • 形態
  • 種族値(HP・攻撃・防御・特攻・特防・素早さ・合計)

目標

新たに追加した情報はこちら:

  • ポケモンの説明、タイトル別(分類ウィジェットをクリックすることでアクセスできる)

ポケモンの説明

  • 進化の流れ

進化の流れ

  • タイプ相性(ダメージ倍率)

タイプ相性表

スキーマ更新

ポケモンスキーマはほとんど変わっていない:

model Pokemon {
  // 全国図鑑の番号
  id             Int         @id
  // 特性
  abilities      String[]
  // 形態
  forms          Form[]
  height         Int
  name           String      @unique
  // 複数言語対応の名前
  names          Name[]
  // URL
  species        String
  sprite         String
  // 種族値
  stats          Stat[]
  type1          String
  type2          String?
  weight         Int
}

しかし、ポケモンスキーマに加え、ポケモン種のスキーマも追加し、進化の流れ情報を入手する:

model Species {
  id                   Int                      @id
  evolutionChain       EvolutionChain?
  evolvesFromSpecies   String?
  flavorTextEntries    SpeciesFlavorTextEntry[]
  genderRate           Int
  // 分類(言語別)
  genera               Genus[]
  name                 String
}

なぜポケモンとポケモン種に分割したかというと、APIデータの設計を真似るためだ。
次に、Genusで分類も取得する:

model Genus {
  id       Int       @id @default(autoincrement())
  genus    String
  language String
  // 複数のポケモンが同じ分類に所属することがよくある(例えば、フシギ一家)
  // そのため、分類と種類の間に1対多のリレーションを設置する
  Species  Species[]
}

最後に、タイプ相性情報を取得するにはタイプスキーマが必要だ:

model Type {
  id               Int         @id
  doubleDamageFrom String[]
  doubleDamageTo   String[]
  halfDamageFrom   String[]
  halfDamageTo     String[]
  noDamageFrom     String[]
  noDamageTo       String[]
  name             String
  // タイプ名(言語別)
  names            TypeName[]
  // 本タイプに分類されるポケモンの名前
  pokemon          String[]
}

// タイプ名(言語別)
model TypeName {
  id       Int    @id @unique @default(autoincrement())
  language String
  name     String
  Type     Type?  @relation(fields: [typeId], references: [id])
  typeId   Int?
}

データ挿入

DBにデータを挿入にはまずAPIからデータを取得しないといけない。さらに、依存関係に基づき、データ取得を行う必要がある。それは、TypeNameを挿入するときtypeIdになるタイプのidがないと無理だからね。

全体的な流れはこちら:

  1. APIからポケモン名をまず取得
  2. 次に一匹のポケモンのデータを取得
  3. ポケモンデータからspeciesフィールドを使い、種類データを取得(speciesはURLだから)
  4. 種類データのidを使ってたくさんのgeneraを生成
  5. 最後のポケモンデータを取得するまで2から4までを繰り返す

タイプデータは同様、タイプ名を取得してからidを使ってTypeNameを生成する。

UIの変更

ポケモンの説明ポップアップ

ポケモンの説明

ポケモンの説明は中央に寄せ、タイトル別に表示したいだ。幸いなことに、データはタイトルの公開年順に並んでいるため、そのままHTML要素に変換すればいいというわけだ:

// flavorTextEntries:タイトル別にポケモンに関する一言を表す変数
// `species`データからもらう
{species.flavorTextEntries
  .filter(({ language }) => language.startsWith('en'))
  .map(entry => (
    <div key={entry.id} className='flex flex-col'>
            {`Pokémon ${entry.version}`}
      {entry.flavorText}
    </div>
))}

ボックス自体も中央に寄せる:

{/* `display: flex`と`justify-center`で子要素中央に寄せる */}
<div className='flex justify-center '>
  {/* 白色の背景にちょっとした影を加えるとだいぶ目立ってくる */}
  <div className='absolute bg-white z-10 w-[40vw] h-[90%] p-[2rem] shadow-lg'>
    // 上記のコード
  </div>
</div>

ポップアップ外にクリックすれば閉じるようにする

ポップアップが開いており、閉じたいとすれば、単にwindowdocumentにイベントリスナーを追加すればいい:

const [show, setShow] = useState(false);

window.addEventListener('mousedown', () => {
  // windowのどこかにクリックすればポップアップを閉じる
  setShow(false);
});

return ({show && (...)})

だが、これではポップアップを開く方法はない。なぜなら開いた側からまた閉じられるからだ。

そこで、クリックイベントがどの要素から発生したのか特定する方法も必要なわけだ。やり方は少なくとも二つある。

子から親へ検索する

例えば、closest(selectors)を使えば、クリックされた要素の親から、selectors(クラスメイなど)に一致する最近要素を見つけることができる。ただ、ここではとりあえずそのような親が存在するかどうか分かれば十分だ:

window.addEventListener('mousedown', e => {
  // イベント元の近くにクラスpopupに所属する要素がなければ、ポップアップの外をクリックした
  if (display && !e.target.closest('.popup')) {
    setShow(false);
  }
});

あとはポップアップにクラスpopupを追加するだけ。

親から子へ検索する

逆にcontains(node)を使い、子要素に指定した要素が含まれているのか特定できる。

const closePopup = (e: MouseEvent) => {
    // ポップアップの子にイベントの対象が含まれていなければ、ポップアップの外をクリックした
    // 註:残念ながら、型をキャストしないといけない
    if (display && !popup.current?.contains(e.target as Node)) {
      setDisplay(false);
    }
  };

また、popup.currentとはuseRefを使った名残りで、再レンダーしてもポップアップへの参照を保持するために活用している。

Discussion