【Next.js + Laravel】野球の試合結果を記録するアプリケーションの開発 #3

## 概要
前回の記事ではバックエンド開発ついて書きましたので今回はフロントエンド開発について記していきたいと思います。

## 環境
- Next.js
- TypeScript
- Styled-components(cssスタイリング)

## プロジェクトの作成
```
npx create-next-app@latest baselog-frontend --typescript
```
styled-componentsの導入
```
npm install styled-components
```
ただインストールするだけだとcssが反映されない場合があったので以下の記事を参考にしました。
参考

https://zenn.dev/takfjp/articles/38f6971a87ee37

## ディレクトリ構成
```
├── node_modules                 
├── public                 
├── src
│   └── app                      
│   │    ├── addgame
│   │    │   └── page.tsx
│   │    ├── games
│   │    │   ├── detail
│   │    │        └── page.tsx
│   │    │   ├── edit
│   │    │        └── page.tsx
│   │    ├── page.tsx
│   │    └── styles
│   │        └── GlobalStyles.ts
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   ├── registry.tsx
│  
├── types                        
│   └── types.ts
└── usecases                    
    └── useAuth.ts
```
ディレクトリ構成は一旦このような感じです。

## 実装
### 試合結果一覧ページの実装
まずは試合結果一覧ページの実装を進めていきます。
```typescript: app/games/page.tsx
'use client';

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import api from '@lib/api';
import type { Game } from 'types/types';

export default function Gamepage() {
    const [games, setGames] = useState<Game[]>([]);

    useEffect(() => {
    const fetchGames = async () => {
      try {
        const response = await api.get<Game[]>('/games');
        setGames(response.data);
      } 
      catch (error) {
        console.error('Error fetching game data:', error);
      }
    };

    fetchGames();
  }, []);
  
  return (
    <Container>
      <Title>試合結果一覧</Title>
      <Table>
        <thead>
          <TableRowHeader>
            <Th>Date</Th>
            <Th>Home</Th>
            <Th>Away</Th>
            <Th>Score</Th>
          </TableRowHeader>
        </thead>
        <tbody>
          {games.map((game) => (
            <TableRow key={game.id}>
              <Td>
                {new Date(game.date).toLocaleDateString()}
              </Td>
              <Td>{game.home_team}</Td>
              <Td>{game.away_team}</Td>
              <Td>{`${game.home_total_score} - ${game.away_total_score}`}</Td>
            </TableRow>
          ))}
        </tbody>
      </Table>
    </Container>
  );
}

//以下スタイルが続く
```
解説
冒頭のこの部分ではuseStateフックを利用して、状態変数gamesと更新関数setGamesを定義しています。初期値として[]が定義されており、Game[]でTypeScriptの型指定を行っています。Gameは型アノテーションでgames配列の各要素がGame型のオブジェクトであることを明示しています。setGamesは状態を更新するための関数でsetGamesを呼び出すとgamesが更新されコンポーネントが再レンダリングされます。
```typescript
const [games, setGames] = useState<Game[]>([]);
```
Game型の定義
```typescript: types.ts
export interface Game {
  id: number;
  date: string;
  home_team: string;
  away_team: string;
  home_total_score: number;
  away_total_score: number;
  home_total_hits: number;
  away_total_hits: number;
  home_total_errors: number;
  away_total_errors: number;
  home_inning_1: number;
  home_inning_2: number;
//以下略
}
```
続いて以下の部分の解説をします。useEffectに渡された関数はレンダーの結果が画面に反映された後に動作します。副作用の処理（DOMの書き換え、変数代入、API通信などUI構築以外の処理）を関数コンポーネントで扱えます。
参考：https://qiita.com/seira/items/e62890f11e91f6b9653f
また、依存配列を空にすることで一度だけ実行するというように指定しています。fetchgamesはAPIからデータを取得する非同期関数として宣言されています。また、try-catch文でエラーハンドリングを行っています。await api.get<Game[]('/games');でAPIエンドポイント/gamesにGETリクエストを送信しています。<Game[]>でレスポンスの型指定を行っています。awaitでリクエストが完了してから次の処理に進むようになっています。setGames(response.data)の部分ではresponse.dataにAPIから取得したGame型の配列が格納されていて、それを状態関数であるgamesに保存するためにsetGamesという更新関数を呼び出しています。response.data と指定するのは、api.get()が返すresponseオブジェクトの中に実際のデータがdataプロパティとして格納されているためです。
```typescript
useEffect(() => {
    const fetchGames = async () => {
      try {
        const response = await api.get<Game[]>('/games');
        setGames(response.data);
      } catch (error) {
        console.error('Error fetching game data:', error);
      }
    };

    fetchGames();
  }, []);
```
参考記事

https://zenn.dev/seetsuko/articles/f378bf513d6231 

https://reffect.co.jp/react/react-hook-usestate-understand

https://qiita.com/honey32/items/62edf5165aced7d0c4bf

https://qiita.com/seira/items/e62890f11e91f6b9653f

https://qiita.com/soarflat/items/1a9613e023200bbebcb3

https://qiita.com/nakajima417/items/4d0c2d46ff82351549e6

### 試合結果追加ページ
続いて、試合結果を追加するページを作成していきます。
まずは試合結果一覧画面に試合結果を追加に遷移するためのボタンを設置しておきます。
```typescript: games/page.tsx
<Link href="/addgame">
    <GameAddButton>試合を追加</GameAddButton>
</Link>
```
そしてaddgame/page.tsxを記述していきます。
```typescript: app/addgame/page.tsx
'use client';

import React, { useState } from 'react';
import styled from 'styled-components';
import api from '@lib/api';
import { useRouter } from 'next/navigation';
import type { GameData } from 'types/types';

//試合を追加するにあたって必要な項目の状態管理
export default function AddGamePage() { 
    const [date, setDate] = useState('');
    const [homeTeam, setHomeTeam] = useState('');
    const [awayTeam, setAwayTeam] = useState('');
    const [homeTotalScore, setHomeTotalScore] = useState(0);
    const [awayTotalScore, setAwayTotalScore] = useState(0);
    const [homeTotalHits, setHomeTotalHits] = useState(0);
    const [awayTotalHits, setAwayTotalHits] = useState(0);
    const [homeTotalErrors, setHomeTotalErrors] = useState(0);
    const [awayTotalErrors, setAwayTotalErrors] = useState(0);
    const [innings, setInnings] = useState(
        Array.from({length: 9}, () => ({home: 0, away: 0}))
    );

    const router = useRouter();

    const handleSave = async() => {
        try {
            const data: GameData = {
            date,
            home_team: homeTeam,
            away_team: awayteam,
            home_total_score: homeTotalScore,
            away_total_score: awayTotalScore,
            home_total_hits: homeTotalHits,
            away_total_hits: awayTotalHits,
            home_total_errors: homeTotalErrors,
            away_total_errors: awayTotalErrors,
            innings.forEach((inning, index) => {
                data[`home_inning_${index + 1}`] = inning.home;
                data[`away_inning_${index + 1}`] = inning.away;
              });
          //dataオブジェクトをPOSTリクエストとしてAPIエンドポイント/gamesに送信し/gamesページにリダイレクトする
          await api.post('/games', data);
          router.push('/games');
        } catch(error) {
          console.error('Error has occured:', error);
        }
      };

      //reduce()で配列の合計値を出す
      const calculateTotalscores = () => {
          setHomeTotalScore(
              innings.reduce((sum, inning) => sum + inning.home, 0)
          );
          setAwayTotalScore(
              innings.reduce((sum, inning) => sum + inning.away, 0)
          );
        };

    return (
    <Container>
      <Title>試合情報の追加</Title>
      <Form>
        <Label>
          Date:
          <Input
            type="date"
            value={date}
            onChange={(e) => setDate(e.target.value)} //eはイベントを引数として受け取る。e.targetはイベントが発生したHTML要素を指す（ここではInput)。valueは現在の値。
          />
        </Label>
        <Label>
          Away Team:
          <Input
            type="text"
            value={awayTeam}
            onChange={(e) => setAwayTeam(e.target.value)}
          />
        </Label>
        <Label>
          Home Team:
          <Input
            type="text"
            value={homeTeam}
            onChange={(e) => setHomeTeam(e.target.value)}
          />
        </Label>

        <Scoreboard>
          <thead>
            <tr>
              <th>Inning</th>
              //スプレッド構文を利用して配列を生成。iが0から8までの値を取るため+1する。
              {[...Array(9)].map((_, i) => (
                <th key={i}>{i + 1}</th>
              ))}
              <th>R</th>
              <th>H</th>
              <th>E</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <Td>Away</Td>
              {innings.map((inning, index) => (
              //Reactのキー属性
                <Td key={index}>
                  <InningInput
                    type="number"
                    value={inning.away}
                    //ユーザーが数値を入力するたびに発生
                    onChange={(e) =>
                      setInnings((prev) => {
                        const updatedInnings = [...prev];
                        //入力値を0以上に制限
                        updatedInnings[index].away =
                          Math.max(
                            0,
                            Number(e.target.value)
                          );
                        calculateTotalscores();
                        return updatedInnings;
                      })
                    }
                  />
                </Td>
              ))}
              <Td>{awayTotalScore}</Td>
              <Td>
                <InningInput
                  type="number"
                  value={awayTotalHits}
                  onChange={(e) =>
                    setAwayTotalHits(
                      Math.max(0, Number(e.target.value))
                    )
                  }
                />
              </Td>
              <Td>
                <InningInput
                  type="number"
                  value={awayTotalErrors}
                  onChange={(e) =>
                    setAwayTotalErrors(
                      Math.max(0, Number(e.target.value))
                    )
                  }
                />
              </Td>
            </tr>
            <tr>
              <Td>Home</Td>
              {innings.map((inning, index) => (
                <Td key={index}>
                  <InningInput
                    type="number"
                    value={inning.home}
                    onChange={(e) =>
                      setInnings((prev) => {
                        const updatedInnings = [...prev];
                        updatedInnings[index].home =
                          Math.max(
                            0,
                            Number(e.target.value)
                          );
                        calculateTotalscores();
                        return updatedInnings;
                      })
                    }
                  />
                </Td>
              ))}
              <Td>{homeTotalScore}</Td>
              <Td>
                <InningInput
                  type="number"
                  value={homeTotalHits}
                  onChange={(e) =>
                    setHomeTotalHits(
                      Math.max(0, Number(e.target.value))
                    )
                  }
                />
              </Td>
              <Td>
                <InningInput
                  type="number"
                  value={homeTotalErrors}
                  onChange={(e) =>
                    setHomeTotalErrors(
                      Math.max(0, Number(e.target.value))
                    )
                  }
                />
              </Td>
            </tr>
          </tbody>
        </Scoreboard>

        <Button onClick={handleSave}>保存</Button>
      </Form>
    </Container>
  );
}
//以下スタイルが続く
```
今回はここまでにしたいと思います。
次回の記事で、試合結果の詳細ページ、編集ページ、削除機能を実装していきます。
## 参考文献
https://qiita.com/seira/items/e62890f11e91f6b9653f

https://zenn.dev/seetsuko/articles/f378bf513d6231 

https://reffect.co.jp/react/react-hook-usestate-understand

https://qiita.com/honey32/items/62edf5165aced7d0c4bf

https://qiita.com/seira/items/e62890f11e91f6b9653f

https://qiita.com/soarflat/items/1a9613e023200bbebcb3

https://qiita.com/nakajima417/items/4d0c2d46ff82351549e6

https://www.commte.co.jp/learn-nextjs/useRouter

https://zenn.dev/kosuke_shopify/articles/95f784531c3c98

https://kinsta.com/jp/knowledgebase/spread-operator-javascript/