1
1

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アプリケーション開発に挑戦してみた!(その23)

0. 初めに

こんにちは!
このシリーズでは、Webアプリケーションの作り方を一から解説しています!

前回からフロントエンド実装編ということで、画面レイアウトに従って、画面を作り始めました。

今回は、ヘッダーを作成したいと思います!

ヘッダーとは

ヘッダーはその名通り、Webページにおいて画面の最上位に位置する領域で、ページのタイトルやロゴ、検索バーや通知、ログイン中のユーザーアイコンなどを表示します。

これらは、Webアプリケーション内のどのページを見ていてもすぐに使いたくなるようなものです。
一番上にこれらを配置することによって、ユーザーの使いやすさや快適さ、いわゆるUI/IXの向上につながります!

例: YouTube
https://www.youtube.com/
image.png

1. ブランチ運用

今日も、ローカルのdevelopを最新化して、新規ブランチを切って作業します。
ブランチ名は、feature/frontend/layout-base/headerにしましょう。

作業が完了したら、コミット・プッシュを忘れずに行いましょう!

細かいコマンドは、前回すべて紹介していますので、もし怪しい場合は確認してみてください。

2. 画面デザイン

前回と同様に実装に入る前に画面デザインを最初に確認します。

今回もFigmaで作成したものを共有いたします。

今回のお手本は以下のような感じです!
image.png

ご覧のように左から、

  • ロゴ
  • ページタイトル
  • ハンバーガーアイコン

の三つを用意する感じでいきたいと思います。

3. コンポーネントへの切り分け

前回Layouts/AppLayout.jsxにて、headerを用意していました。

\project-root\src\resources\js\Layouts\AppLayout.jsx
export default function AppLayout({ children }) {
  return (
    <div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
      {/* Header */}
      <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
        <h1 className="text-lg font-semibold">ヘッダー領域</h1>
      </header>

      {/* Main Content Area */}
      <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>
    </div>
  );
}

このままこの中身を修正していってもよいのですが、このheader自体を別の場所に移動させて、それをLayouts/AppLayout.jsxで呼び出すという形を取りたいと思います。

こうすることで、仮に今後headerの中身が肥大化していっもLayouts/AppLayout.jsxのファイルの行数がそれほど多くならず、保守性が上がります。
このような考え方をコンポーネントの切り分けと呼びます!
つまり、部品ごとに役割を分けて、上位の部品は下位の部品を組み合わせることに集中するようにするということです。

新しく、Components/Header.jsxというファイルを作成しましょう。

\project-root\src\resources\js\Components\Header.jsx
const Header = () => {
  return (
    <header>
      {/* ここにAppLayout.jsx内のheader要素の中身を貼り付けます */}
    </header>
  );
};

export default Header;

前回の復習になりますが、Reactでは関数コンポーネントというものを用意して、それをエクスポート・インポートして使いまわすことができるのでした。

もともとのAppLayout.jsx内にあるheaderの内容を返す関数を変数Headerに代入してエクスポートします。

\project-root\src\resources\js\Components\Header.jsx
const Header = () => {
  return (
    <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
      <h1 className="text-lg font-semibold">ヘッダー領域</h1>
    </header>
  );
};

export default Header;

これを、AppLayoutの方からインポートして使えばOKです!
もともとあったheaderは削除しないとヘッダーが二つ表示されていしまうので注意です。

import Header from "../Components/Header"; // 追加

export default function AppLayout({ children }) {
  return (
    <div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
      {/* Header */}
      <Header /> {/* 追加。もともとあったheaderは削除 */}

      {/* Main Content Area */}
      <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>
    </div>
  );
}

例によって、AppLayoutが適用されているマイページで確認しましょう。
http://localhost/mypage
image.png
問題なさそうですね!(≧◇≦)

※ちなみに前回書きそびれてしまったのですが、Reactでの注意点としてクラス属性を指定するときは、classではなく、classNameと書かないといけません。

4. ロゴ作成

今日、この後はHeader.jsxの中にいろいろと追加していきます!
まずは、ロゴです。

ロゴはアプリ全体の印象を決める重要な部分です。
覚えてもらえるようなものにしたいですよね。

また、クリックすることでトップページに移動できるようにしておくと、ユーザーからも使いやすいと思われるはずです!

事前にロゴの画像を用意しましたので、ダウンロードして使ってください。
ChatGPTとFigmaを組み合わせて作りました!
header.svg

以下のコマンドを実行して、新しいディレクトリを用意してください。
実行コマンド

/project-root
$ mkdir -p src/resources/js/Assets/logo

このディレクトリ内にダウンロードした画像ファイルを入れてください(VS Codeにドラッグ & ドロップが一番簡単かもです)。

出来たら、Header.jsxで呼び出します。

\project-root\src\resources\js\Components\Header.jsx
import logo from '../Assets/logo/header.svg';

const Header = () => {
  return (
    <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
      <div className="flex items-center">
        <img src={logo} alt="App Logo" className="h-9 w-auto" />
      </div>
    </header>
  );
};

export default Header;

本来であれば、画像ファイルはこんな感じにインポートできるものではないのですが、本シリーズで採用しているViteという実行環境だと、こんな感じに書くことで、logoという変数に画像ファイルのパスが代入されます。

それによって、src={logo}のように展開するだけで画像をimgタグの中で設定できます!

以下のようになっていれば完璧です!
image.png

さらに、もう一歩アプリを使いやすくしたい方は、トップページへ遷移するためのリンクも貼っておくとよいでしょう!

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

const Header = () => {
  return (
    <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
      <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>
    </header>
  );
};

route('labs.home')と書くことで、バックエンド編で作ったweb.phpに書かれているルーティングを呼び出せます!

\project-root\src\routes\web.php
Route::get('/', [LabController::class, 'home'])->name('labs.home'); // <-これ

aria-label='トップページへ'の部分は、視覚障がい者の方が使うスクリーンリーダーでの自動読み上げのためのものです。
レイアウトには関係ないので、そこまで気にしなくて大丈夫です!

ロゴをクリックするとトップページに移動できることを確認しましょう!
image.png

5. ページタイトル作成

次に、ページタイトルを作成したいと思います。
画面の最上位にあるヘッダーの真ん中にページのタイトルがあれば、今どのページを開いているのかがユーザーから一目でわかってよいと思います!

共通レイアウトとしてのヘッダーなのにどうやってページごとにタイトルを変えるのかと言いますと、例のpropsというものを用います。

少し難しそうに聞こえるかもしれませんが、異なるコンポーネント間でやり取りする変数みたいなものだと思ってもらえればよいかなって思います!

本シリーズでは、Inertitaを用いているのでInertia propsを活用して、Laravel側から設定しているタイトルを最終的にはHeaderに渡すという流れにしたいと思います。

データの流れとしては、以下のイメージです。

MyPageController -> MyPage/Index-> AppLayout -> Header

ということで、MyPageController.phpInertiaを使って、タイトル情報をpropsMyPage/Index.jsxに渡すように修正しましょう。

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

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Inertia\Inertia;

class MyPageController extends Controller
{
    public function showUser()
    {
        $user = Auth::user();

        // 管理者・一般ユーザー問わず通知を取得
        $notifications = $user->notifications()->latest()->get();

        return Inertia::render('MyPage/Index', [
            'title' => "{$user->name}さんのマイページ", // 追加
            'user' => $user,
            'notifications' => $notifications,
        ]);
    }
    
    // ...

フロントエンド編なのにバックエンドのコード変えるのかよ!
と思ったかもしれません。

まあ、そういうときもありますよ。(笑)
特にInertiaだとバックエンドとフロンエンドが密に関係性を持っているので行ったり来たりになってしまうのは仕方ないと思います。('_')

実際、バックエンド実装編の時点でReactを作っていましたしね。

続いて、このtitleMyPage/Index.jsxで受け取って、共通レイアウトへ送りましょう。

\project-root\src\resources\js\Pages\MyPage\Index.jsx
import AppLayout from '@/Layouts/AppLayout'; // 追加: AppLayoutをインポート
import { Head, Link, router } from '@inertiajs/react';

export default function Index({ user, notifications = [], title }) { // 追加: titleをpropsとして受け取る
    const handleDeleteAccount = () => {
        if (confirm('本当に退会しますか?この操作は取り消せません。')) {
            router.delete(route('mypage.delete'));
        }
    };

    const unreadCount = notifications.filter(n => !n.read_at).length;

    return (
        <AppLayout title={title}>
            {/* 今まで「マイページ」を設定していたHeadは削除 */}

            <div className="space-y-4">
                <div className="text-gray-800">
                    <div className={user.is_admin ? 'text-red-500' : ''}>
                        未読の通知: {unreadCount}</div>
                    <Link href={route('notifications.index')}>
                        <button className="mt-2 px-4 py-1 bg-gray-200 rounded-md hover:bg-gray-300">
                            通知一覧を見る
                        </button>
                    </Link>
                </div>

                <h3 className="text-lg font-semibold">ユーザー情報</h3>
                <p>名前: {user.name}</p>
                <p>メールアドレス: {user.email}</p>
                <p>登録日: {new Date(user.created_at).toLocaleDateString('ja-JP')}</p>

                <div className="space-x-2">
                    <Link href={route('mypage.edit')}>
                        <button className="px-3 py-1 border rounded-md hover:bg-gray-100">編集する</button>
                    </Link>

                    <Link href={route('mypage.bookmarks')}>
                        <button className="px-3 py-1 border rounded-md hover:bg-gray-100">ブックマーク済み研究室</button>
                    </Link>

                    <button
                        onClick={handleDeleteAccount}
                        className="px-3 py-1 border rounded-md text-red-600 hover:bg-red-50"
                    >
                        退会する
                    </button>
                </div>
            </div>
        </AppLayout>
    );
}

<AppLayout title={title}>とすることで、MyPageControllerから受け取ったtitleAppLayouttitleへ渡します(どちらもtitleなのでわかりにくいですね。その場合は、片方を別の変数名にしても動くことを確認してみてください)。

次に、AppLayoutでそのtitleを受け取って、それをさらにHeaderに渡すようにします。

\project-root\src\resources\js\Layouts\AppLayout.jsx
import { Head } from '@inertiajs/react';
import Header from "../Components/Header"; // 追加

export default function AppLayout({ children, title }) { // 追加: titleをpropsとして受け取る
  return (
    <div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
      <Head title={title} />
      {/* Header */}
      <Header title={title}/>

      {/* Main Content Area */}
      <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>
    </div>
  );
}

一つ前と同様に、<Header title={title}/>とすることで、MyPage/Indexから受け取ったtitleHeadertitleというpropsとして渡すことができます。

ちなみに、<Head title={title} />とすることで、ブラウザ上でのページタイトルを指定することができます。
ブラウザのタブの部分に表示されるものです。
↓これです。
image.png

最後に、Headertitleを受け取って、表示しましょう。
h1タグを使うのが良いかなと思います。

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

const Header = ({ title }) => {
  return (
    <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
      <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>
    </header>
  );
};

export default Header;

こんな感じになっていればOK!!
image.png

6. ハンバーガーアイコン作成

さてさて、お次はハンバーガーアイコンを用意しましょう!
少しお腹がすいてきますね。(´・ω・)

こんな感じのやつです。
例: YouTube
https://www.youtube.com/
image.png

この一番上の三本戦のやつをハンバーガーメニューと呼んだりしまして、ここをクリックすると次回作る予定のサイドバーが開くという感じにしたいと思います。

画像は以下をお使いください!
hamburger.svg

React Iconsというサイトから探すとよいものが見つかります。
https://react-icons.github.io/react-icons/

ここからダウンロードして、Figmaで手を加えたものが今回使用する画像です。

\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 }) => {
  return (
    <header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center justify-between px-6">
      <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>

      <button
        className="flex items-end"
        aria-label="メニューを開く"
      >
        <img src={hamburgerIcon} alt="メニュー" className="h-7 w-7 hover:opacity-80 transition" />
      </button>
    </header>
  );
};

export default Header;

ホバー時の変化も付けて起きました。

また、ロゴ、タイトルとの配置が良くなるように、headerclassName属性にjustify-betweenを追加しておきましたので、ご注意ください!

こんな感じになっていれば結構!!
image.png

7. 境界線のフェード作成

ここから先は、おまけなのでお腹いっぱいの方は、コミット・プッシュして終了でも大丈夫です。(笑)

見本とまだ違う点として、下方向の境界線の色がついていません。
これを付けて今日は完成としましょう~!

本来グラデーションは、linear-gradientというCSSプロパティを使うのですが、Tailwind CSSだとできないので、headerの下線というよりは、線を書き加えることで下線であるように見せます。

最終的な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 }) => {
  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>

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

export default Header;

after疑似要素と呼ばれるもので、要素っぽいけどHTMLの要素にはならない装飾のためだけのものとして使うことができます。
気になる方は、「CSS 疑似要素」とかで調べてみましょう!

【気になる方だけ】疑似要素について調べてみましょう。

こんな感じになります。
image.png

...どこかで見たことがあるって?

そうですよ!!
ポケポケのデザインが気に入ったので、今回デザインを作るうえでかなり意識させていただきました!(というかパ〇りました

8. ☆コラム ~Prettierを使おう~

さらに、おまけですが、みなさんはコードフォーマッターというものを使っていますか?

特にHTMLなどのネスト(入れ子構造のこと)が深くなりがちなものを書いているときに、自動でインデントをそろえてくれるツールを使うと便利です。

Prettierは、VS Codeの拡張機能で、インストールをすると、「Ctrl」+「Alt」+「F」でコードをきれいに整えてくれます。

環境構築編の時に、伝えそびれてしまいましたが、便利なのでぜひインストールして使ってみてください。

ところが、自動成型の結果が見づらい時ってあると思います。
自分の期待通りのフォーマットにしたいですよね。

そんな時は、package.jsonなどと同じ階層に.prettierrcというJSON形式の設定ファイルを作成すると便利です。
僕が採用している設定は以下の通りです。
良ければ使ってみてください。

\project-root\src.prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "avoid",
  "bracketSpacing": true,
  "jsxSingleQuote": false,
  "endOfLine": "auto"
}

コードフォーマッターPrettierを使ってみよう!

9. まとめ・次回予告

お疲れ様でした!

作業が終わったら、コミット・プッシュをお忘れなくです。('_')

今日は、共通レイアウトととして、ヘッダーを作成しました。

まず、ロゴを作ることで、アプリ全体のイメージを印象付け、トップページに戻りやすくすることでより良いUI/UXを実現しました。

次に、ページタイトルを真ん中に設けることで、自分が今どのページを見ているのかが一目でわかるようになりました。

さらに、ハンバーガーアイコンを付けることで、サイドバーを開く準備をしました。
サイドバーは次回作りますので、お楽しみに!(≧◇≦)

最後に、変なパクリ疑惑も出ましたが、見た目を整えました。

次回もよろしくお願いします!
ありがとうございました。

軽く宣伝

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

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

これまでの記事一覧

☆要件定義・設計編

☆環境構築編

☆バックエンド実装編

☆フロントエンド実装編

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?