見出し画像

laravel breeze(+inertia.js, react)に通知機能を与える(1)準備


今一つ知られてるのか知られていないのかよくわからん機能にlaravelの通知作成支援機能がある。それで作ってみるわけだがinertia.jsとreactの場合ちょっとlayout系を操作するにはクセがあるので、まずそこをシミュレートしようということになる。

ちなみにavatarからの流れてavatarがひっついているが、まあ気にしないでおk

通知ベルを作る

react-iconが入ってるという前提。ここではVscを使っているがまあ何でもいい、好きなやつでどーぞ。

import { VscBell } from 'react-icons/vsc';

export default function Authenticated({ user, header, children }) {
//
  return (
//
            <div className="hidden sm:flex sm:items-center sm:ml-6">
              <div className="mr-3 relative">
                {/* Notification Bell Icon */}
                <span className="inline-flex rounded-md">
                  <button
                  type="button"
                  className="inline-flex items-center p-2 ml-3 border border-transparent rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
                >
                    <VscBell className="h-6 w-6" />
                  </button>
                </span>
              </div>
              <div className="relative">
                <Dropdown>
                  <Dropdown.Trigger>

ただし、この箇所はsm(small device)では表示されない。これは後で調整しよう。とりあえずPCでは見えているはずだ


通知bellが出現した

propsを渡す

layoutにpropsを渡す場合は実はapp/Http/Middleware/HandleInertiaRequests.php を参照する必要がある。

    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user(),
            ],
            'ziggy' => function () use ($request) {
                return array_merge((new Ziggy)->toArray(), [
                    'location' => $request->url(),
                ]);
            },
            // 'notificationsCount' => $request->user()->unreadNotifications->count(), // 未読通知の数
            'notificationsCount' => 5,
        ]);
    }

ここではnotificationsCountに実際のlaravelの通知支援を書いているんだけど、まだ実装していない。従って適当な数字を渡してテストしてみよう。たとえば5とする

これを取得し、表示してみよう。

import { useState } from 'react';
import { Link, usePage } from '@inertiajs/react';
import Avatar from 'react-avatar';
import ApplicationLogo from '@/Components/ApplicationLogo';
import Dropdown from '@/Components/Dropdown';
import NavLink from '@/Components/NavLink';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
import { VscBell } from 'react-icons/vsc';

export default function Authenticated({ user, header, children }) {
  const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
  const notificationsCount = usePage().props.notificationsCount;


// ...
                  <button
                  type="button"
                  className="inline-flex items-center p-2 ml-3 border border-transparent rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
                >
                    <VscBell className="h-6 w-6" />
                    {notificationsCount && (<b>{notificationsCount}</b>)}
                  </button>


5が表示されている

通知の数が変わる問題

ここで、たとえばなんか適当なpostを受け付けるrouteを作ってみよう。

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::post('/dashboard', function () {
    return redirect()->back();
})->middleware(['auth', 'verified'])->name('dashboard');

ここでDashboardにボタンを付けて疑似的にpostを発生させる。まあ別にこれはrouter.postでもいいけど何となくformを書いた

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import PrimaryButton from '@/Components/PrimaryButton';

export default function Dashboard({ auth }) {
    const { post, processing} = useForm();
    const submit = (e) => {
        e.preventDefault();
        post(route('dashboard'));
    };
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <Head title="Dashboard" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                        <div className="p-6 text-gray-900">You're logged in!</div>

                        <form onSubmit={submit}>
                          <PrimaryButton className="ml-4" disabled={processing}>
                            Click
                          </PrimaryButton>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

Clickボタン

じゃあこのClickを押したときに擬似的に通知というか数を増やすためにセッションの値をインクリメンタルしてみよう。

Route::post('/dashboard', function () {
    $notificationsCount = $request->session()->get('notificationsCount', 0);
    $request->session()->put('notificationsCount', $notificationsCount + 1);
    return redirect()->back();
})->middleware(['auth', 'verified'])->name('dashboard');

この値を、とりあえずDashboardで確認しておく。

use Illuminate\Http\Request;
Route::get('/dashboard', function (Request $request) {
    $count = $request->session()->get('notificationsCount', 0);
    return Inertia::render('Dashboard', ['count' => $count]);
})->middleware(['auth', 'verified'])->name('dashboard');
Route::post('/dashboard', function (Request $request) {
    $notificationsCount = $request->session()->get('notificationsCount', 0);
    $request->session()->put('notificationsCount', $notificationsCount + 1);
    return redirect()->back();
})->middleware(['auth', 'verified'])->name('dashboard');

viewに渡しているので、一応出しておこう。

export default function Dashboard({ auth, notificationsCount }) {

<p>{notificationsCount}</p>

で出している。やりすぎな感じだとこんな

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import PrimaryButton from '@/Components/PrimaryButton';

export default function Dashboard({ auth, count }) {
    const { post, processing} = useForm();
    const submit = (e) => {
        e.preventDefault();
        post(route('dashboard'));
    };
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <Head title="Dashboard" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                        <div className="p-6 text-gray-900">You're logged in!</div>

                        <div className="flex m-4">
                            <span className="px-2 py-1 text-sm font-bold leading-none text-red-100 bg-red-600 rounded-full">
                                {count}
                            </span>
                        </div>

                        <form onSubmit={submit}>
                          <PrimaryButton className="ml-4" disabled={processing}>
                            Click
                          </PrimaryButton>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}


やりすぎ赤丸デザイン

このアニgifじゃ初期値が10だったけど、とにかくこのようにclickすると数字がincrementされていく。この値を通知の数として得てみよう

通知の数としての利用

当然、ここでもsessionから値を取り出す必要がある。これは

Route::get('/dashboard', function (Request $request) {
    $count = $request->session()->get('notificationsCount', 0);

ここで行ったような処理だ

ここではsessionから
app/Http/Middleware/HandleInertiaRequests.php 

    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user(),
            ],
            'ziggy' => function () use ($request) {
                return array_merge((new Ziggy)->toArray(), [
                    'location' => $request->url(),
                ]);
            },
            // 'notificationsCount' => $request->user()->unreadNotifications->count(), // 未読通知の数
            'notificationsCount' => $request->session()->get('notificationsCount', 0),
        ]);
    }

このように取得してpropsに与えている。


Bellの数字が増えているが実際にはClickボタンを押しているぞ!

画面を縮小するとこのbellがみえねえからw今この部分だけしか出してないんだが、clickボタンに連動して数字が上がっていってるとおもってくださいな(ヘンなアニgifがないほうがわかりやすいかな?あるいはちゃんとレスポンシブにしろってことか…)

デザインの調整

increment表示の上限を設けて+で表示

まあ、通知の数をガンガン上げていってもいいっちゃいいんだけどある程度のところまで来たら10+とか100+とかにするのが今っぽい。

ってわけで関数を作って

  const formatNotificationCount = (count) => {
    return count > 10 ? '10+' : count.toString();
  };

適用する

<button
  type="button"
  className="inline-flex items-center p-2 ml-3 border border-transparent rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
>
  <VscBell className="h-6 w-6" />
  {notificationsCount && (<b>{formatNotificationCount(notificationsCount)}</b>)}
</button>

cssの調整

bellの隣に雑然と数字があるだけだと今っぽくないので…

<button
  type="button"
  className="inline-flex items-center p-2 ml-3 border border-transparent rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
>
  <VscBell className="h-6 w-6" />
  <span className="absolute -top-1 -right-1 inline-flex items-center justify-center h-6 w-6 text-xs font-bold text-white bg-red-500 rounded-full border-2 border-white">
    {formatNotificationCount(notificationsCount)}
  </span>
</button>


それっぽくなりましたね

ポーリングの挑戦

まあこれはいずれ後でやりますか

次回以降

フロントエンドは「まあまあ」固まったのでbackendをやっていく。フロントが固まったといってもまだbellの部分だけなので通知エリアがdropdownするようなものすらないが、これは流石にリアルなエントリがあった方がわかりやすいかとは思うので。


この記事が気に入ったらサポートをしてみませんか?