0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(フロントエンド実装編④)~ホームページ作成~

Posted at

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その25)

0. 初めに

前回までで共通レイアウトが完成しました!
主にAppLayout.jsxにコードを書いていきました。

実際に用意したのは、メインコンテンツ領域ヘッダーサイドバーの三つでした。

今回からは、これら三つを適用して、それぞれのページを作っていきたいと思います!

今日作るのは、アプリを開いたら最初に表示されるホームページです!

1. ブランチ運用

例によって、developブランチを最新化して、新規ブランチを切って作業していきましょう。
ブランチ名は、feature/frontend/home-pageが良いでしょう。

作業が終わったら、いつも通り、コミット・プッシュをお忘れなくです!

2. 仕様変更

これまでの、バックエンド実装編では、ホームページを研究室一覧ページにしていました。

しかし、学生ユーザーの目線に立ってよく考えてみると、自分が通っていない別の大学の研究室の情報が最初に出てきても、あまり興味はないかなと感じました。
それよりは、まず自分の通っている大学を検索してほしいと思いました。

もちろん、大学を検索する機能は既にありますが、よりシンプルにした方が使いやすいかなと思いました。

そのため、今回は、フロントエンド編でありながら、バックエンドのソースコードの修正も行っていきます!(≧◇≦)

ご了承ください!"(-""-)"

3. 画面デザイン

では、いつも通りお手本となる画面デザインを確認してから実装に入っていきましょう!

まずは、研究室一覧ページだった従来のホームページは、こんな感じでした。

(Before)
image.png

(After)
image.png

どうでしょうか?
仕様変更後の方がより洗練された感じで、すっきりして見やすいでしょう!

極力この通りに作れるように、今日も最後まで頑張っていきましょうね。(^_-)-☆

4. コントローラー作成

現状だと、ホームページを表示する処理は、LabContollerに任せています。

\project-root\src\app\Http\Controllers\LabController.php
    public function home()
    {
        $labs = Lab::with(['faculty.university'])->get();
        return Inertia::render('Lab/Home', [
            'labs' => $labs,
        ]);
    }

このhome()メソッドを何か別のホームページに関する処理をするためのコントローラーに移したほうが役割の責務分担が明確になってよいと思います!

現状、そのようなコントローラーはないので、新規に作成したいと思います。

PHPのDockerコンテナの中に入って、以下のコマンドで作成しましょう。

/var/www
$ php artisan make:controller HomeController

先ほどのLabController内にあったhome()メソッドを「Ctrl」+「x」で切り取って、「Ctrl」+「v」で張り付けましょう。
ついでに、Inertiaのuse宣言も追加しておきましょう。

\project-root\src\app\Http\Controllers\HomeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;

class HomeController extends Controller
{
    public function home()
    {
        $labs = Lab::with(['faculty.university'])->get();
        return Inertia::render('Lab/Home', [
            'labs' => $labs,
        ]);
    }
}

研究室一覧を表示する必要がないため、研究室情報を渡す処理はもはや必要なくなりました。
そのため、その部分を削除しましょう。
また、表示するページも変えましょう(Home.jsxはこの後作成します)。

\project-root\src\app\Http\Controllers\HomeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;

class HomeController extends Controller
{
    public function home()
    {
        return Inertia::render('Home');
    }
}

かなりシンプルになりましたね!

5. ルーティング修正

コントローラーを変更したので、このままだとエラーが出てしまいます。
ルーティングを修正して、正しくコントローラーのメソッドが呼び出されるようにしましょう!

\project-root\src\routes\web.php
// ホームページ(修正前)
Route::get('/', [LabController::class, 'home'])->name('labs.home');

// ホームページ(修正後)
Route::get('/', [HomeController::class, 'home'])->name('home');

HomeControllerのuse宣言もお忘れなく!

\project-root\src\routes\web.php
use App\Http\Controllers\HomeController; // 追加

6. Reactコンポーネント作成

バックエンドの修正は以上です。
ここからいよいよページを作成していきましょう!!

ホームページ用のReactコンポーネントを新規作成する

今のままだと、「Home.jsxが見つかりませんよ」というエラーが出てしまいます。

コントローラーで参照していたページ用コンポーネントをLab/Home.jsxからHome.jsxに変えましたが、このHome.jsxはまだ作っていないので当然です。

まずは、以下のコマンドを実行して作成しましょう。

/project-root
$ touch ./src/resources/js/Pages/Home.jsx

作成出来たら、中身を書きます。
最初に、使いそうなモジュールをインポートしておきます。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";

前回から本格的に使っているReactの標準フックであるuseState、さらに検索フォーム用にルーティングを発動できるInertiaのrouterを呼び出します。

また、前回までで作成した共通レイアウトのAppLayoutも呼び出しておきましょう。

次に、いつものように関数コンポーネントの定義・エクスポートの宣言をしましょう。
名前は、ファイル名に即してHomeとします。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";

export default function Home({ query=''}) {
    
}

受け取るpropsとしては、検索する単語であるqueryを空文字をデフォルト引数として設定しておきます。

デフォルト引数については、このシリーズでも過去に登場した気がするので、もし分からない方は復習しておきましょう!
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Default_parameters

(怪しい方のみ)デフォルト引数について復習しましょう。

この関数コンポーネントの中身を書いていきましょう。
まずは、useStateを使って、検索する文字の状態を管理します。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || ''); // 追加
}

||は、論理和演算子と呼ばれる書き方です。
こちらもおそらくこのシリーズでは、何度か登場しているので復習しておきましょう!
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_OR

(怪しい方のみ)論理演算子について復習しましょう。

続いて、検索文字列をクエリとして送信するための関数を定義します。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 追加: 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }
}

ここまで準備出来たら、JSXの部分を書いていきましょう。
まずは、インポートしているApplayoutを適用しましょう。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }
}

// 追加
return (
  <AppLayout>

  </AppLayout>
);

この中にお手本で確認した必要なものたちを書いていきましょう。

  • ロゴ
  • 検索バー
  • メッセージ(「まずは大学を検索してみましょう。」)

一つ目は、ロゴです!
最初のページの中央にロゴがあるとインパクトがあってよいでしょう!

:\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import logo from "../Assets/logo/Home.svg"; // 追加

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }

  return (
    <AppLayout>
      {/* 追加 */}
      <div className="flex min-h-[calc(100vh-80px)] flex-col items-center justify-center">
        <img src={logo} alt="App Logo" className="h-9 w-auto" />
      </div>
    </AppLayout>
  )
};

さて、動作確認しようっと...
image.png

...ってめちゃエラー出とるやんけ!( ;∀;)

「F12」キーを押すと、ブラウザ上でコンソールエラーを見ることができるのでした!
画面がうまく表示できないときとかに、ぜひ活用してください。

エラー文を読むとどうやら、'labs.home'とかHeader.jsxが関係ありそう...
試しにHeader.jsxを見てみると...

\project-root\src\resources\js\Components\Header.jsx
import logo from '../Assets/logo/header.svg';
import hamburgerIcon from '../Assets/icons/hamburger.svg';
import { Link } from '@inertiajs/react';

const Header = ({ title, onOpenSidebar }) => { // 追加: onOpenSidebar を受け取る
  return (
    <header
      className="
        relative
        h-14
        flex
        items-center
        justify-between
        px-6
        bg-[#EEF5F9]
        after:content-['']
        after:absolute
        after:bottom-0
        after:left-0
        after:h-[3px]
        after:w-full
        after:bg-[linear-gradient(to_right,rgba(226,145,140,0.8),rgba(215,145,232,0.8),rgba(118,192,235,0.8))]
      "
    >
      {/* 左:ここ!! ロゴ */}
      <div className="flex items-center">
        <Link href={route('labs.home')} aria-label="トップページへ">
          <img src={logo} alt="App Logo" className="h-9 w-auto" />
        </Link>
      </div>

      {/* 中央:タイトル */}
      <h1 className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-black">
        {title}
      </h1>

      {/* 右:ハンバーガー onClickにonOpenSidebarを追加 */}
      <button
        className="ml-auto flex items-center"
        aria-label="メニューを開く"
        onClick={onOpenSidebar}
      >
        <img
          src={hamburgerIcon}
          alt="メニュー"
          className="h-7 w-7 hover:opacity-80 transition"
        />
      </button>
    </header>
  );
};

export default Header;

あ!前々回作ったときにロゴに設定していたルーティングの名前がそのままになっていました!

これによって、「'labs.home'なんていうルーティング名はねぇぞ」というエラーが出てしまったようです。

該当箇所を先ほどweb.phpで設定した名前に変えておきましょう。

\project-root\src\resources\js\Components\Header.jsx
      {/* 左:ロゴ */}
      <div className="flex items-center">
        <Link href={route('home')} aria-label="トップページへ">
          <img src={logo} alt="App Logo" className="h-9 w-auto" />
        </Link>
      </div>

前回作ったSidebar.jsxにも同様のことが起きているので、修正しておきましょう。

メニュー定義のホームの部分ですね。
以下のようにしましょう。

\project-root\src\resources\js\Components\Sidebar.jsx
  // メニュー定義
  const items = isLoggedIn
    ? [
        { label: 'ホーム', href: route('home'), icon: home }, // ここと
        { label: 'マイページ', href: route('mypage.index'), icon: mypage },
        { label: 'ブックマーク', href: route('mypage.bookmarks'), icon: bookmark },
        { label: 'ランキング', href: null, icon: ranking },
        { label: 'ログアウト', href: route('logout'), method: 'post', icon: logout },
      ]
    : [
        { label: 'ホーム', href: route('home'), icon: home }, // ここだよ
        { label: '新規登録', href: route('register'), icon: register },
        { label: 'ログイン', href: route('login'), icon: login },
        { label: 'ランキング', href: null, icon: ranking },
      ];

これで、エラーが消えるはずです!
image.png

...って小っさ!!(+_+)
この辺の微調整は最後にまとめてやろうかなと思います。

次は、検索バーを作りましょう。
本格的なレイアウトは後で作るとして、まずはformタグの中にinputタグを作るという王道のパターンで作ってみましょう!

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import logo from "../Assets/logo/Home.svg";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }

  return (
    <AppLayout>
      {/* ロゴ */}
      <img src={logo} alt="App Logo" className="h-9 w-auto" />
      {/* 追加: 検索フォーム */}
      <form
        onSubmit={handleSubmit}
        className="mt-10 w-full max-w-md mx-auto"
      >
        <input
          type="text"
          value={search}
          onChange={e => setSearch(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              handleSubmit(e)
            }
          }}
          placeholder="大学名を入力..."
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </form>
    </AppLayout>
  )
};

詳しい説明は、後ほどにしますが、onChangeイベントにsetSearch()を呼び出すコールバック関数を指定することで、いわゆるデータバインディングを実現しています。

また、「Enter」キー押下で検索を実行できるようになっています。
image.png
image.png

試しに、何か検索してみました!
うまくいっているみたいでよかったです。

最後は、メッセージ(「まずは大学を検索してみましょう。」)を追加です。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import logo from "../Assets/logo/Home.svg";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }

  return (
    <AppLayout>
      {/* ロゴ */}
      <img src={logo} alt="App Logo" className="h-9 w-auto" />
      {/* 検索フォーム */}
      <form
        onSubmit={handleSubmit}
        className="mt-10 w-full max-w-md mx-auto"
      >
        <input
          type="text"
          value={search}
          onChange={e => setSearch(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              handleSubmit(e)
            }
          }}
          placeholder="大学名を入力..."
          className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </form>
      {/* 追加: 検索メッセージ */}
      <p>まずは大学を検索してみましょう。</p>
    </AppLayout>
  )
};

こんな感じです。
色も大きさも位置もすべてがバラバラですな。((´∀`*))ヶラヶラ
image.png

AppLayoutを修正する

共通レイアウトは前回までで完成しただろ!なぜ今更AppLayoutを修正する必要があるんだ!!

と思ったかもしれません。

これには深~いわけがあるのですよ。
すんません。大して深くないっす...

お手本と現状で大きく異なっていることの一つとして、ヘッダーの有無があると思います。

前々回作ったヘッダーの管理は、AppLayoutでされています。
そのため、AppLayoutを適用している以上、ホームページにもヘッダーが現れてしまいます。

しかし、今回、ホームページに限っては、ヘッダーを非表示にしたいです。

多くのWebサイトが、ホームページだけは、他のページとあえてレイアウトを変えるということをしています。
そうすることで、ホームページが際立ち、ユーザーに対して「これからアプリケーションを使い始めるのだな」という気持ちにさせることができます。

では、どうすればよいのかと考えたのですが、いろいろ方法があると思います。
今回は、AppLayoutにホームページがそれ以外かというモードを持たせて、それによって、ヘッダーの表示の有無を切り替えたいと思います!

ハンバーガーアイコンのコンポーネント切り出し

ただし、一点課題があります。
それが、「ホームページは、ヘッダーは表示したくないけど、ハンバーガーアイコン押下によるサイドバーの開閉操作はできるようにしたい」というなんともわがままな要望。

そのためには、これまでHeader内に埋まっていたハンバーガーアイコンをコンポーネントとして切り出したほうがやりやすそうなので、そうしたいと思います。

ということで、新規コンポーネントファイルを作成しましょう。

\project-root\src\resources\js\Components\HamburgerMenu.jsx
import hamburgerIcon from '../Assets/icons/hamburger.svg';

export default function HamburgerMenu({ onOpenSidebar }) {
  return (
    <button
      onClick={onOpenSidebar}
      className="ml-auto flex items-center"
      aria-label="メニューを開く"
    >
      <img
        src={hamburgerIcon}
        alt="メニュー"
        className="h-7 w-7 hover:opacity-80 transition"
      />
    </button>
  );
}

Header.jsxを修正して、このHamburgerMenuにpropsを渡すようにしましょう。

\project-root\src\resources\js\Components\Header.jsx
import logo from '../Assets/logo/header.svg';
import HamburgerMenu from './HamburgerMenu'; // 修正
import { Link } from '@inertiajs/react';

const Header = ({ title, onOpenSidebar }) => {
  return (
    <header
      className="
        relative
        h-14
        flex
        items-center
        justify-between
        px-6
        bg-[#EEF5F9]
        after:content-['']
        after:absolute
        after:bottom-0
        after:left-0
        after:h-[3px]
        after:w-full
        after:bg-[linear-gradient(to_right,rgba(226,145,140,0.8),rgba(215,145,232,0.8),rgba(118,192,235,0.8))]
      "
    >
      {/* 左:ロゴ */}
      <div className="flex items-center">
        <Link href={route('home')} aria-label="トップページへ">
          <img src={logo} alt="App Logo" className="h-9 w-auto" />
        </Link>
      </div>

      {/* 中央:タイトル */}
      <h1 className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-black">
        {title}
      </h1>

      {/* 修正: 右:ハンバーガーアイコンメニュー */}
      <HamburgerMenu onOpenSidebar={onOpenSidebar} />
    </header>
  );
};

export default Header;

切り出しはこれで完了です!
サイドバーの開閉がこれまでと同様に問題なくできることを確認してください。

AppLayoutへのモード付与

いよいよAppLayoutへのモード付与を行います!

\project-root\src\resources\js\Layouts\AppLayout.jsx
import { Head, usePage } from '@inertiajs/react';
import { useState, useCallback, useEffect } from 'react';
import Header from "../Components/Header";
import Sidebar from "../Components/Sidebar";
import HamburgerMenu from '@/Components/HamburgerMenu';

export default function AppLayout({ children, title, mode='default' }) { // 追加: mode prop を受け取る
  // ユーザーの認証状態を管理
  const { props } = usePage();
  const isLoggedIn = !!props?.auth?.user;

  // サイドバーの開閉状態を管理
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const setSidebarOpen = useCallback((open) => setIsSidebarOpen(Boolean(open)), []);

  // 追加:ホームページかどうか
  const isHome = mode === 'home';

  // 追加: サイドバーが開いている間は、背景のスクロールを防止
  useEffect(() => {
    if (isSidebarOpen) document.body.classList.add('overflow-hidden');
    else document.body.classList.remove('overflow-hidden');
    return () => document.body.classList.remove('overflow-hidden');
  }, [isSidebarOpen]);

  // 追加: Escキーでサイドバーを閉じる
  useEffect(() => {
    const onKey = (e) => e.key === 'Escape' && setIsSidebarOpen(false);
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  return (
    <div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
      <Head title={title} />

      {/* 修正: ホームモード: ハンバーガーアイコンのみを固定表示(サイドバー非表示時のみ) */}
      {isHome ? (
        !isSidebarOpen && (
          <div className="fixed top-4 right-6 z-50">
            <HamburgerMenu onOpenSidebar={() => setSidebarOpen(true)} />
          </div>
        )
      ) : (
        /* デフォルト: フルヘッダー表示 */
        <Header title={title} onOpenSidebar={() => setIsSidebarOpen(true)} />
      )}

      {/* メインコンテンツ領域 */}
      <main className="flex-1 bg-transparent flex">
        <div
          className="
            mx-auto
            w-full
            max-w-container
            px-[clamp(16px,4vw,32px)]
            py-6
            bg-[#EEF5F9]
            flex-1
          "
        >
          {children || <p className="text-gray-400 text-center">メインコンテンツ領域</p>}
        </div>
      </main>
      {/* サイドバー */}
      <Sidebar isOpen={isSidebarOpen} onClose={() => setSidebarOpen(false)} isLoggedIn={isLoggedIn} />
    </div>
  );
}

共通レイアウトを使う際は、modeというpropsを渡す必要があるようにしました。
これで、Home.jsxhomeをpropsとして渡せば、isHometrueになり、表示が切り替えられます!

一方で、他のページでhome以外であることをpropsとして渡すように修正するのは大変(まだそこまででもないっすけどw)なので、デフォルト引数を設定することで、Home.jsx以外でAppLayout.jsxを使う際には、modeの受け渡しを省略できるようにしました!

動作確認

ホームページでは、ハンバーガーアイコンだけで、ヘッダーが表示されません。
image.png

検索結果ページには、ヘッダーが表示されます。
image.png

こうなっていればOK!!

検索フォームをコンポーネントに切り出す

続いて、取り組みたいのが、検索フォームをコンポーネントに切り出す作業です。

お手本に近づけられるように検索のアイコンを付けたりします。

\project-root\src\resources\js\Components\SearchBar.jsx
import searchInputIcon from '../Assets/icons/search/icon-search-input.svg';
import searchButtonIcon from '../Assets/icons/search/icon-search-button.svg';

export default function SearchBar({
  value,
  onChange,
  onSubmit,
  placeholder = '大学名を入力...',
}) {
  const isDisabled = !value || !value.trim();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (isDisabled) return;
    onSubmit?.(e);
  };

  return (
    <form
      onSubmit={handleSubmit}
      className='w-full max-w-xl mx-auto'
    >
      <div className='
        flex items-center
        rounded-lg
        bg-[#E2EDF6]
        border border-[#E2EDF6]
        shadow-inner
        px-4 py-0
      '>
        {/* 左虫眼鏡アイコン */}
        <div className='mr-0.5 flex-shrink-0'>
          <img src={searchInputIcon} alt="" className='w-5 h-5' />
        </div>
        {/* 入力フィールド */}
        <input
          type="text"
          value={value}
          onChange={onChange}
          placeholder={placeholder}
          className="flex-grow bg-transparent border-none focus:outline-none focus:ring-0 text-black placeholder-[#747D8C]"
        />
        {/* 区切り線 + 右側の検索ボタン */}
        <div className='flex items-center'>
          <div className='
            h-6
            border-l border-[#747D8C]
            mx-3
          '></div>
          <button
            type="submit"
            disabled={isDisabled}
            className={`
              flex items-center justify-center
              w-6 h-6
              rounded-md
              focus:outline-none focus:ring-2 focus:ring-blue-500
              ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}
            `}
          >
            <img src={searchButtonIcon} alt="検索" className='w-6 h-6' />
          </button>
        </div>
      </div>
    </form>
  )
}

Home.jsxでこれを呼ぶように修正します。
もともとあった、formは削除しましょう。

\project-root\src\resources\js\Pages\Home.jsx
import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import logo from "../Assets/logo/Home.svg";
import SearchBar from "../Components/SearchBar";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }

  return (
    <AppLayout mode="home"> {/* ホームモードをpropsで渡す */}
      {/* ロゴ */}
      <img src={logo} alt="App Logo" className="h-9 w-auto" />
      {/* 検索フォーム */}
      <SearchBar
        value={search}
        onChange={e => setSearch(e.target.value)}
        onSubmit={handleSubmit}
      />
      {/* 検索メッセージ */}
      <p>まずは大学を検索してみましょう。</p>
    </AppLayout>
  )
};

少しはマシになったかな!
image.png

先ほど、説明を後回しにしたいわゆるデータバインディングについて解説します。

検索バーの方では、入力文字列を表示する値をvalueとしています。

\project-root\src\resources\js\Components\SearchBar.jsx
        {/* 入力フィールド */}
        <input
          type="text"
          value={value}
          onChange={onChange}
          placeholder={placeholder}
          className="flex-grow bg-transparent border-none focus:outline-none focus:ring-0 text-black placeholder-[#747D8C]"
        />

これは、propsとして、Home.jsxから受け取っています。

\project-root\src\resources\js\Components\SearchBar.jsx
export default function SearchBar({
  value,
  onChange,
  onSubmit,
  placeholder = '大学名を入力...',
}) {
// ...

Home.jsxでは、valueに対して、useStateで管理しているsearchという当たを渡しています。

\project-root\src\resources\js\Pages\Home.jsx
        {/* 検索フォーム */}
        <SearchBar
          value={search}
          onChange={e => setSearch(e.target.value)}
          onSubmit={handleSubmit}
        />

再び、検索バーの方に戻ってみてみると、値が変化するときにonChangeが呼び出されており、これもpropsでHome.jsxから渡されています。

\project-root\src\resources\js\Components\SearchBar.jsx
        {/* 入力フィールド */}
        <input
          type="text"
          value={value}
          onChange={onChange}
          placeholder={placeholder}
          className="flex-grow bg-transparent border-none focus:outline-none focus:ring-0 text-black placeholder-[#747D8C]"
        />

またまた、Home.jsxに戻ってみてみるとonChangeにはvalueの更新関数を渡しています。

\project-root\src\resources\js\Pages\Home.jsx
        {/* 検索フォーム */}
        <SearchBar
          value={search}
          onChange={e => setSearch(e.target.value)}
          onSubmit={handleSubmit}
        />

...つまり、どういうことだってばよ・・・??

  1. ユーザーがSearchBarに値を入力
  2. SearchBaronChangeイベントが発火
  3. それにより、Homeがpropsとして渡しているsetSearchが発動
  4. これにより、searchが変わり、valueがpropsで渡される
  5. これをSearchBarが受け取り、valueが更新されて表示される

要するに、ユーザーが文字を入力するとstateが更新されて、それがすぐさま入力フィールドに表示されるというわけです。
ちょっと難しかったかもですね💦

なんとなく雰囲気だけでもつかんでもらえたら、幸いです。

レイアウトを調整する

最後は、ロゴとメッセージの位置や大きさ、色がおかしなことになっているので、直します!

import { useState } from "react";
import { router } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import logo from "../Assets/logo/Home.svg";
import SearchBar from "../Components/SearchBar";

export default function Home({ query=''}) {
  const [search, setSearch] = useState(query || '');

  // 検索フォームを送信する関数
  const handleSubmit = e => {
    e.preventDefault();
    router.get('/universities', { query: search });
  }

  return (
    <AppLayout mode="home"> {/* ホームモードをpropsで渡す */}
      <div className="flex flex-col items-center gap-8 pt-24">

        {/* ロゴ */}
        <img src={logo} alt="App Logo" className="h-32 w-auto" />

        {/* 検索フォーム */}
        <SearchBar
          value={search}
          onChange={e => setSearch(e.target.value)}
          onSubmit={handleSubmit}
        />

        {/* 検索メッセージ */}
        <p className="text-[#747D8C]">まずは大学を検索してみましょう。</p>
      </div>
    </AppLayout>
  )
};

じゃじゃん!
かっこいい感じになりましたね。(≧◇≦)
image.png

7. まとめ・次回予告

お疲れ様でした!

今回から、前回までに作った共通レイアウトを用いて各ページを作り始めました!

今日は、ホームページを作成しました!

AppLayoutにホームページかどうかを判定するモードを付与することで、ホームページの時だけヘッダーを非表示にするというひと工夫をしましたね!

次回は、検索結果の画面を作りたいと思います。(^。^)
コミット・プッシュを忘れないようにしておきましょう。

これまでの記事一覧

☆要件定義・設計編

☆環境構築編

☆バックエンド実装編

☆フロントエンド実装編

軽く宣伝

YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。

現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?