実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その25)
0. 初めに
前回までで共通レイアウトが完成しました!
主にAppLayout.jsxにコードを書いていきました。
実際に用意したのは、メインコンテンツ領域、ヘッダー、サイドバーの三つでした。
今回からは、これら三つを適用して、それぞれのページを作っていきたいと思います!
今日作るのは、アプリを開いたら最初に表示されるホームページです!
1. ブランチ運用
例によって、developブランチを最新化して、新規ブランチを切って作業していきましょう。
ブランチ名は、feature/frontend/home-pageが良いでしょう。
作業が終わったら、いつも通り、コミット・プッシュをお忘れなくです!
2. 仕様変更
これまでの、バックエンド実装編では、ホームページを研究室一覧ページにしていました。
しかし、学生ユーザーの目線に立ってよく考えてみると、自分が通っていない別の大学の研究室の情報が最初に出てきても、あまり興味はないかなと感じました。
それよりは、まず自分の通っている大学を検索してほしいと思いました。
もちろん、大学を検索する機能は既にありますが、よりシンプルにした方が使いやすいかなと思いました。
そのため、今回は、フロントエンド編でありながら、バックエンドのソースコードの修正も行っていきます!(≧◇≦)
ご了承ください!"(-""-)"
3. 画面デザイン
では、いつも通りお手本となる画面デザインを確認してから実装に入っていきましょう!
まずは、研究室一覧ページだった従来のホームページは、こんな感じでした。
どうでしょうか?
仕様変更後の方がより洗練された感じで、すっきりして見やすいでしょう!
極力この通りに作れるように、今日も最後まで頑張っていきましょうね。(^_-)-☆
4. コントローラー作成
現状だと、ホームページを表示する処理は、LabContollerに任せています。
public function home()
{
$labs = Lab::with(['faculty.university'])->get();
return Inertia::render('Lab/Home', [
'labs' => $labs,
]);
}
このhome()メソッドを何か別のホームページに関する処理をするためのコントローラーに移したほうが役割の責務分担が明確になってよいと思います!
現状、そのようなコントローラーはないので、新規に作成したいと思います。
PHPのDockerコンテナの中に入って、以下のコマンドで作成しましょう。
$ php artisan make:controller HomeController
先ほどのLabController内にあったhome()メソッドを「Ctrl」+「x」で切り取って、「Ctrl」+「v」で張り付けましょう。
ついでに、Inertiaのuse宣言も追加しておきましょう。
<?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はこの後作成します)。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
class HomeController extends Controller
{
public function home()
{
return Inertia::render('Home');
}
}
かなりシンプルになりましたね!
5. ルーティング修正
コントローラーを変更したので、このままだとエラーが出てしまいます。
ルーティングを修正して、正しくコントローラーのメソッドが呼び出されるようにしましょう!
// ホームページ(修正前)
Route::get('/', [LabController::class, 'home'])->name('labs.home');
// ホームページ(修正後)
Route::get('/', [HomeController::class, 'home'])->name('home');
HomeControllerのuse宣言もお忘れなく!
use App\Http\Controllers\HomeController; // 追加
6. Reactコンポーネント作成
バックエンドの修正は以上です。
ここからいよいよページを作成していきましょう!!
ホームページ用のReactコンポーネントを新規作成する
今のままだと、「Home.jsxが見つかりませんよ」というエラーが出てしまいます。
コントローラーで参照していたページ用コンポーネントをLab/Home.jsxからHome.jsxに変えましたが、このHome.jsxはまだ作っていないので当然です。
まずは、以下のコマンドを実行して作成しましょう。
$ touch ./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とします。
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を使って、検索する文字の状態を管理します。
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
(怪しい方のみ)論理演算子について復習しましょう。
続いて、検索文字列をクエリとして送信するための関数を定義します。
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を適用しましょう。
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>
);
この中にお手本で確認した必要なものたちを書いていきましょう。
- ロゴ
- 検索バー
- メッセージ(「まずは大学を検索してみましょう。」)
一つ目は、ロゴです!
最初のページの中央にロゴがあるとインパクトがあってよいでしょう!
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>
)
};
...ってめちゃエラー出とるやんけ!( ;∀;)
「F12」キーを押すと、ブラウザ上でコンソールエラーを見ることができるのでした!
画面がうまく表示できないときとかに、ぜひ活用してください。
エラー文を読むとどうやら、'labs.home'とかHeader.jsxが関係ありそう...
試しに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で設定した名前に変えておきましょう。
{/* 左:ロゴ */}
<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にも同様のことが起きているので、修正しておきましょう。
メニュー定義のホームの部分ですね。
以下のようにしましょう。
// メニュー定義
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 },
];
...って小っさ!!(+_+)
この辺の微調整は最後にまとめてやろうかなと思います。
次は、検索バーを作りましょう。
本格的なレイアウトは後で作るとして、まずはformタグの中にinputタグを作るという王道のパターンで作ってみましょう!
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」キー押下で検索を実行できるようになっています。


試しに、何か検索してみました!
うまくいっているみたいでよかったです。
最後は、メッセージ(「まずは大学を検索してみましょう。」)を追加です。
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>
)
};
こんな感じです。
色も大きさも位置もすべてがバラバラですな。((´∀`*))ヶラヶラ

AppLayoutを修正する
共通レイアウトは前回までで完成しただろ!なぜ今更AppLayoutを修正する必要があるんだ!!
と思ったかもしれません。
これには深~いわけがあるのですよ。
すんません。大して深くないっす...
お手本と現状で大きく異なっていることの一つとして、ヘッダーの有無があると思います。
前々回作ったヘッダーの管理は、AppLayoutでされています。
そのため、AppLayoutを適用している以上、ホームページにもヘッダーが現れてしまいます。
しかし、今回、ホームページに限っては、ヘッダーを非表示にしたいです。
多くのWebサイトが、ホームページだけは、他のページとあえてレイアウトを変えるということをしています。
そうすることで、ホームページが際立ち、ユーザーに対して「これからアプリケーションを使い始めるのだな」という気持ちにさせることができます。
では、どうすればよいのかと考えたのですが、いろいろ方法があると思います。
今回は、AppLayoutにホームページがそれ以外かというモードを持たせて、それによって、ヘッダーの表示の有無を切り替えたいと思います!
ハンバーガーアイコンのコンポーネント切り出し
ただし、一点課題があります。
それが、「ホームページは、ヘッダーは表示したくないけど、ハンバーガーアイコン押下によるサイドバーの開閉操作はできるようにしたい」というなんともわがままな要望。
そのためには、これまでHeader内に埋まっていたハンバーガーアイコンをコンポーネントとして切り出したほうがやりやすそうなので、そうしたいと思います。
ということで、新規コンポーネントファイルを作成しましょう。
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を渡すようにしましょう。
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へのモード付与を行います!
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.jsxでhomeをpropsとして渡せば、isHomeがtrueになり、表示が切り替えられます!
一方で、他のページでhome以外であることをpropsとして渡すように修正するのは大変(まだそこまででもないっすけどw)なので、デフォルト引数を設定することで、Home.jsx以外でAppLayout.jsxを使う際には、modeの受け渡しを省略できるようにしました!
動作確認
ホームページでは、ハンバーガーアイコンだけで、ヘッダーが表示されません。

こうなっていればOK!!
検索フォームをコンポーネントに切り出す
続いて、取り組みたいのが、検索フォームをコンポーネントに切り出す作業です。
お手本に近づけられるように検索のアイコンを付けたりします。
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は削除しましょう。
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>
)
};
先ほど、説明を後回しにしたいわゆるデータバインディングについて解説します。
検索バーの方では、入力文字列を表示する値をvalueとしています。
{/* 入力フィールド */}
<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から受け取っています。
export default function SearchBar({
value,
onChange,
onSubmit,
placeholder = '大学名を入力...',
}) {
// ...
Home.jsxでは、valueに対して、useStateで管理しているsearchという当たを渡しています。
{/* 検索フォーム */}
<SearchBar
value={search}
onChange={e => setSearch(e.target.value)}
onSubmit={handleSubmit}
/>
再び、検索バーの方に戻ってみてみると、値が変化するときにonChangeが呼び出されており、これもpropsでHome.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の更新関数を渡しています。
{/* 検索フォーム */}
<SearchBar
value={search}
onChange={e => setSearch(e.target.value)}
onSubmit={handleSubmit}
/>
...つまり、どういうことだってばよ・・・??
- ユーザーが
SearchBarに値を入力 -
SearchBarのonChangeイベントが発火 - それにより、
Homeがpropsとして渡しているsetSearchが発動 - これにより、
searchが変わり、valueがpropsで渡される - これを
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>
)
};
7. まとめ・次回予告
お疲れ様でした!
今回から、前回までに作った共通レイアウトを用いて各ページを作り始めました!
今日は、ホームページを作成しました!
AppLayoutにホームページかどうかを判定するモードを付与することで、ホームページの時だけヘッダーを非表示にするというひと工夫をしましたね!
次回は、検索結果の画面を作りたいと思います。(^。^)
コミット・プッシュを忘れないようにしておきましょう。
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
- その2: 環境構築編① ~WSL, Ubuntuインストール~
- その3: 環境構築編② ~Docker Desktopインストール~
- その4: 環境構築編③ ~Dockerコンテナ立ち上げ~
- その5: 環境構築編④ ~Laravelインストール~
- その6: 環境構築編⑤ ~Gitリポジトリ接続~
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"






