react✖️laravelでカレンダーを作成してみる

### はじめに

個人開発メモ📝
カレンダーページに予定を反映させる実装にチャレンジしてみたので、備忘録として残します。

### 参考サイト

今回参考にさせてもらったサイト
https://freemas.org/front/react/laravel_react_spa1-3
カレンダービューの作成の考え方などとても参考になりました！

### 全体図

今回のざっくりとした設計全体図は⇩

![IMG_0207.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513569/7b9da729-20fb-cd95-16c6-efec01166942.jpeg)


### 実装内容

#### バックエンド側

```app/Http/Controllers/ScheduleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Schedule;

class ScheduleController extends Controller {
  public function store(Request $request)
  {
    /**
     * 検証後、データベースにフォームデータを登録
     * 
     * @param Request $request
     * @return Response
     */
    $scheduleForm = (object)$request->validateWithBag('post', [
      /**
       * task: validationルールを設定する
       */
      'date' => 'required',
      'requirement' => 'required',
      'memo' => '',
    ]);

    // エラーハンドリング
    if($scheduleForm === 'error') {
      return redirect('/schedule')
        ->withErrors($scheduleForm)
        ->withInput();
    }

    // validationチェック後、データベースに登録
    $newScheduleForm = Schedule::create([
      'date' => $request->input('date'),
      'requirement' => $request->input('requirement'),
      'memo' => $request->input('memo'),
    ]);

    // 登録されたデータから日付を取得
    $selectedDate = $newScheduleForm['date'];

    // 指定された日付に紐づくデータを取得
    $formFilterData = Schedule::whereDate('date', $selectedDate)->get();

    return response()->json([
      'message' => 'スケジュールが登録されました',
      'selectedDate' => $selectedDate,
      'formFilterData' => $formFilterData,
    ], 200);
  } 
}
```

コントローラー内では、リクエストデータから日付を取得し、その日付が入ったカラム内から関連するデータを返すようにしています。⇩
```
// 登録されたデータから日付を取得
$selectedDate = $newScheduleForm['date'];

// 指定された日付に紐づくデータを取得
$formFilterData = Schedule::whereDate('date', $selectedDate)->get();
```

```app/database/migrations/create_schedules_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('schedules', function (Blueprint $table) {
            $table->id()->comment('ユーザーID');
            $table->date('date')->comment('予定日時');
            $table->string('requirement')->comment('要件');
            $table->string('memo')->comment('メモ')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('schedules');
    }
};
```

```app/Models/Schedule.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Schedule extends Model
{
  /**
   * テーブルに関連付ける主キー
   * 
   * @var string
   */
  protected $primaryKey = 'user_id';

  /**
   * IDを自動増分しない
   * 
   * @var bool
   */
  public $incrementing = false;
  
  /**
   * 複数代入可能にする
   * 
   * @var array
   */
  protected $fillable = [
    'date',
    'requirement',
    'memo',
  ];

  /**
   * 外部キーを使用のため、Userテーブルと関連付ける
   */
  public function userId()
  {
    return $this->belongsTo(User::class);
  }
}
```

#### フロントエンド側

```resources/js/Schedule.jsx
import React, { useState } from 'react';
import Schedule from "../scss/schedule.module.scss";
import { postFormData } from './Feature';

const ScheduleForm = ({ passToResponseData }) => {
  // フォームデータ変数定義
  const [scheduleFormData, setScheduleFormData] = useState({
    date: '',
    requirement: '',
    memo: '',
  });

  // 入力内容の反映
  const handleChange = (event) => {
    const { name, value } = event.target;
    setScheduleFormData({ ...scheduleFormData, [name]: value});
  };

  // Feature.jsxにあるpostFormData()を呼び出す
  const handleInputSubmit = async(event) => {
    event.preventDefault();
    // csrfトークン取得
    const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
    // responseに、バックエンドからのレスポンスが代入される
    const response = await postFormData({
      csrfToken,
      data: scheduleFormData,
    });
    if(response !=null) {
      await passToResponseData(response);
    } else {
      console.log('データがありません');
    }
  };

  return(
    <div>
      <div className={ Schedule.schedule }>
        <form className={ Schedule.schedule__form }
              onSubmit={ handleInputSubmit }
        >
          <label className={ Schedule.schedule__label }>
            日付：
            <input  type="date"
                    name="date"
                    placeholder="日付の選択をしてください"
                    className={ Schedule.schedule__content }
                    onChange={ handleChange }
            />
          </label>
          <label className={ Schedule.schedule__label }>
            要件：
            <input  type="text"
                    name="requirement"
                    placeholder="内容を入力してください"
                    className={ Schedule.schedule__content }
                    onChange={ handleChange }
            />
          </label>
          <label className={ Schedule.schedule__label }>
            メモ：
            <textarea className={ Schedule.schedule__content }
                      name="memo"
                      onChange={ handleChange }
            >
            </textarea>
          </label>
          <div>
            <input  type="submit"
                    value="作成"
            />
          </div>
        </form>
      </div>
    </div>
  );
};

export default ScheduleForm;
```

```Feature.jsx
// APIルート
const scheduleApiUrl = '/api/schedule';

// フォームデータの送信・レスポンスの取得
export const postFormData = async({ csrfToken, data }) => {
  try {
    const response = await fetch(scheduleApiUrl, {
      method: 'POST',
      headers: {
        'CONTENT-TYPE': 'application/json',
        'X-CSRF-TOKEN': csrfToken,
      },
      body: JSON.stringify(data)
    });
    if(!response.ok) {
      throw new Error('エラーが出ました');
    };
    return response.json();
  } catch(error) {
      console.log(error);
      return undefined;
  };
};
```

```Calender.jsx
const CalenderUserPage = React.memo(function() {

  省略

  // Schedule.jsxから値が返ってくるので取得・更新し、フォームを閉じる
  const [responseViewData, setResponseViewData] = useState(null);
  const [formToFilterData, setFormToFilterData] = useState([]);
  const passToResponseData = async(data) => {
    setResponseViewData(data);
    setOpenForm(!openForm);
  };
  // レスポンスデータが渡ってきたら検知し、必要なデータのみセットする
  useEffect(() => {
    if(responseViewData && responseViewData.selectedDate && responseViewData.formFilterData) {
      setFormToFilterData(responseViewData.formFilterData);
    }
  }, [responseViewData]);

  {/* ビュー部分 */}
  省略
  
  // ユーザー選択日とビュー上の日にちが一致するか調べる
  const dayData = formToFilterData.filter(item => {
    // ユーザー選択日を取得する
    const itemDate = new Date(item.date).getDate();
    return itemDate === day;
  });

  省略

  {dayData.map(item => (
    <p key={item.id} className={Calender.calender__detail}>{item.requirement}</p>
  ))}

省略
```

カレンダービュー部分は、コードが長いので、ユーザーが選択した日付にデータを反映させる部分のみ載せています。
フロント側では、レスポンスデータをカレンダービューまで渡し、必要データを取得⇨filter()でデータ内から日付を取り出し、ビュー上の日付と一致するか確認する処理を行う⇨一致する日付に紐づいたデータを反映させています。

### 挙動

![スクリーンショット 2023-11-23 19.17.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513569/cd156dd0-ac27-12ad-bec7-2a47febca4c8.png)


![スクリーンショット 2023-11-23 19.18.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3513569/174d59cc-03e7-29c1-c21d-e5f6d6ab1ad2.png)
デザイン整えます・・


### まとめ

カレンダーをライブラリを使わず手動で作成するとどうなるのか興味があり、チャレンジしましたが、理解が追いつかず難しい...！
ですが、データ反映のためにリクエストデータの処理やフロントの記述の仕方など勉強になることがとても多かったです。