laravelの認証機構を見てみよう(12) : userのcrudを仕上げる(5) - ユーザーのpagingとcomponent化


もうちょいで仕上がると思う!

ユーザーのpagingとcomponent

とりあえずユーザーは増えてくるだろうから、

database/seeders/DatabaseSeeder.php

        // ユーザーにadminロールを割り当て
        $admin1->assignRole('admin');
        $admin2->assignRole('admin');

        User::factory(50)->create();
    }

みたいな感じでとりあえず50人作ってみよう。


このようにズラズラ生成されるとまあさすがにキツいわけだ。これのpagingを行ってみよう。

paginateは

ここでやっているね。これを参考にしてみよう

    public function index(): Response
    {
        // $users = User::with('roles')->get();
        $users = User::with('roles')->orderBy('id', 'asc')->paginate(20);

        return Inertia::render('Users/Index', ['users' => $users]);
    }

このように変更してコンポーネントの

{users.map((user, index) => (

{users.data.map((user, index) => (

に変更するのだった。まだpagerは書いてない

pagerの設置

{users.links && (
    <nav className="flex justify-end space-x-4 my-4">
        {users.links.map((link, index) => (
            <Link
                key={index}
                href={link.url || '#'}
                className={
                    link.active
                        ? 'px-4 py-2 bg-blue-500 text-white border border-blue-500 rounded'
                        : 'px-4 py-2 hover:underline border border-transparent rounded hover:border-gray-300'
                }
            >
                <span dangerouslySetInnerHTML={{ __html: link.label }} />
            </Link>
        ))}
    </nav>
)}

このようにするわけだ

が、同じようなコードが頻出しすぎる問題

ページャーとかはActivityLogと共通だったりするしというのがあるので、ちょっとコードを見直していこう。

まず新規ユーザー作成ボタンはPrimaryButtonというコンポーネントが用意されているので、なるべくそれをつかうべきであろう。それはサイトデザインを統一するのにも向いている。

<Link href={route('users.create')} className="text-blue-600 hover:text-blue-900 flex items-center">
    <button className="bg-blue-500 hover:bg-blue-700 text-white py-1 px-2 rounded flex items-center">
        <VscPersonAdd className="mr-2" />{t("Create New User")}
    </button>
</Link>

ただ、これLinkの中にButtonが入ってたりとしかしてもともとヘンなんすよね。動くは動くけど…

PrimaryButtonを使って正しく記載すると

<PrimaryButton onClick={() => router.get(route('users.create'))} >
    <VscPersonAdd className="mr-2" />{t("Create New User")}
</PrimaryButton>

などとなるかもしれないがonClickをイチイチ各のは面倒くさいから、このようにできるようにしよう

<PrimaryButton href={route('users.create')}>
    <VscPersonAdd className="mr-2" />{t("Create New User")}
</PrimaryButton>

これを正しく扱えるようにコンポーネントを変更する

export default function PrimaryButton({ className = '', disabled, children, href, ...props }) {
    const buttonClass = `inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150 ${disabled && 'opacity-25'} ` + className;

    if (href) {
        return (
            <a
                href={href}
                className={buttonClass}
                {...props}
            >
                {children}
            </a>
        );
    }

    return (
        <button
            {...props}
            className={buttonClass}
            disabled={disabled}
        >
            {children}
        </button>
    );
}

のようにしておけばhrefがあった場合はaタグに切り替えてくれるだろう

            <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 bg-white border-b border-gray-200 flex justify-end">
                            <PrimaryButton href={route('users.create')}>
                                <VscAdd className="mr-2" />{t("Create New User")}
                            </PrimaryButton>
                        </div>

VscPersonAddやめてVscAddにしれっと変更しましたw

ページャーコンポーネント

レイアウト面も含めるか含めないか問題とかあるんやろうけどまあこの場合はどーんといってみよう

import { Link } from '@inertiajs/react';
export default function PaginationNav({ links }) {
    if (!links) return null;

    return (
        <nav className="flex justify-end space-x-4 my-4">
            {links.map((link, index) => (
                <Link
                    key={index}
                    href={link.url || '#'}
                    className={
                        link.active
                            ? 'px-4 py-2 bg-blue-500 text-white border border-blue-500 rounded'
                            : 'px-4 py-2 hover:underline border border-transparent rounded hover:border-gray-300'
                    }
                >
                    <span dangerouslySetInnerHTML={{ __html: link.label }} />
                </Link>
            ))}
        </nav>
    );
}
<PaginationNav links={users.links} />

で呼び出せる


フォントとかでかいなーと思ったらコンポーネント側を修正したらいい

import { Link } from '@inertiajs/react';
export default function PaginationNav({ links }) {
    if (!links) return null;

    return (
        <nav className="flex justify-end space-x-2 my-2">
            {links.map((link, index) => (
                <Link
                    key={index}
                    href={link.url || '#'}
                    className={
                        link.active
                            ? 'px-2 py-1 bg-blue-500 text-white border border-blue-500 rounded text-sm'
                            : 'px-2 py-1 hover:underline border border-transparent rounded hover:border-gray-300 text-xs'
                    }
                >
                    <span dangerouslySetInnerHTML={{ __html: link.label }} />
                </Link>
            ))}
        </nav>
    );
}


thead

                <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 bg-white border-b border-gray-200 flex justify-end">
                            <PrimaryButton href={route('users.create')}>
                                <VscAdd className="mr-2" />{t("Create New User")}
                            </PrimaryButton>
                        </div>
                        <PaginationNav links={users.links} />

                        <table className="min-w-full divide-y divide-gray-200 m-3 mb-20">
                            <thead className="bg-gray-50">
                                <tr>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("ID")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Email")}
                                    </th>

これも問答無用でコンポーネントにしてもいいくらいズラズラ書かれているけど、まあこれはちょっと置いときますわ今回はね。

リンクの表示とEmail Verify

<td className="px-6 py-4 whitespace-nowrap flex items-center">
    <Link
        href={route('users.show', user.id)}
        className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 flex items-center">
        {user.email_verified_at !== null ? (
            <VscVerifiedFilled className="text-green-500 mr-2 text-xl flex-shrink-0" />
        ) : (
            <VscUnverified className="text-red-500 mr-2 text-lg flex-shrink-0" />
        )}
        <span>{user.email}</span>
    </Link>
</td>

このようになってる箇所はまずアプリケーション内で通常リンク表示としてのコンポーネントを作成したい

resources/js/Components/AppLink.jsx

import { Link } from '@inertiajs/react';

export default function AppLink({ href, children, className }) {
    return (
        <Link
            href={href}
            className={`underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 flex items-center ${className}`}
        >
            {children}
        </Link>
    );
}

のように装飾されたリンクを作成しておけば

<td className="px-6 py-4 whitespace-nowrap flex items-center">
    <AppLink href={route('users.show', user.id)}>
        {user.email_verified_at !== null ? (
            <VscVerifiedFilled className="text-green-500 mr-2 text-xl flex-shrink-0" />
        ) : (
            <VscUnverified className="text-red-500 mr-2 text-lg flex-shrink-0" />
        )}
        <span>{user.email}</span>
    </AppLink>
</td>

このようにしておける。さらにはverificationにおいてはEmailWithStatusとかいうのを作る

resources/js/Components/EmailWithStatus.jsx

import { VscVerifiedFilled, VscUnverified } from "react-icons/vsc";

export default function EmailWithStatus({ email, isVerified }) {
    return (
        <div className="flex items-center text-lg text-gray-500">
            {isVerified ? (
                <VscVerifiedFilled className="text-green-500 mr-2 text-xl" />
            ) : (
                <VscUnverified className="text-red-500 mr-2 text-lg" />
            )}
            <span>{email}</span>
        </div>
    );
}

最終的な呼び出し(importは書いてね)

<td className="px-6 py-4 whitespace-nowrap flex items-center">
    <AppLink href={route('users.show', user.id)}>
        <EmailWithStatus email={user.email} isVerified={user.email_verified_at !== null} />
    </AppLink>
</td>

roleの表示

<td className="px-6 py-4 whitespace-nowrap">
    {user.roles && user.roles.length > 0 ? (
        user.roles.map((role, index) => (
            <span
            key={index}
            className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
        >
                {role.name}
        </span>
        ))
    ) : (
        <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-600">
            No roles
        </span>
    )}
</td>

この部分は、Show.jsx にてロールの表示で被ってるのでこれはやっぱコンポーネントとしたい。

これはこんな感じで呼び出せるようにしとくか

<td className="px-6 py-4 whitespace-nowrap">
    <RoleDisplay roles={user.roles} />
</td>

resources/js/Components/RoleDisplay.jsx

import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function RoleDisplay({ roles }) {
    const { t } = useLaravelReactI18n();
    if (!roles || roles.length === 0) {
        return (
            <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-600">
                {t("No roles")}
            </span>
        );
    }

    return (
        <>
            {roles.map((role, index) => (
                <span
                    key={index}
                    className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
                >
                    {role.name}
                </span>
            ))}
        </>
    );
}

なーんとなく多言語にもしておいた。No roleでもいいかとも思うんすけどねw

last loginの表示

まあこれもLastLoginDisplayとかにしますか

resources/js/Components/LastLoginDisplay.jsx

import dayjs from 'dayjs';
import { useLaravelReactI18n } from 'laravel-react-i18n';

function LastLoginDisplay({ lastLoginAt }) {
    const { t } = useLaravelReactI18n();

    return (
        <>
            {lastLoginAt ? (
                <>
                    <span>{dayjs(lastLoginAt).format('YYYY-MM-DD HH:mm:ss')}</span>
                    <small className="ml-2 text-sm text-gray-600">({dayjs(lastLoginAt).fromNow()})</small>
                </>
            ) : (
                <span className="text-gray-500">{t("Never")}</span>
            )}
        </>
    );
}

export default LastLoginDisplay;
<td className="px-6 py-4 whitespace-nowrap">
    <LastLoginDisplay lastLoginAt={user.last_login_at} />
</td>

的な

Dropdown

<td className="px-6 py-4 whitespace-nowrap">
    <Dropdown>
        <Dropdown.Trigger>
            <button>
                <VscEllipsis />
            </button>
        </Dropdown.Trigger>
        <Dropdown.Content>
            <Link href={route('users.edit', user.id)} className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out">
                {t("Edit")}
            </Link>
            <button onClick={() => handleDelete(user.id)} className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out">
                {t("Delete Account")}
            </button>
        </Dropdown.Content>
    </Dropdown>
</td>

まずLinkは既にコンポーネントが実はありました

<td className="px-6 py-4 whitespace-nowrap">
    <Dropdown>
        <Dropdown.Trigger>
            <button>
                <VscEllipsis />
            </button>
        </Dropdown.Trigger>
        <Dropdown.Content>
            <Dropdown.Link href={route('users.edit', user.id)}>
                {t("Edit")}
            </Dropdown.Link>
            <button onClick={() => handleDelete(user.id)} className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out">
                {t("Delete Account")}
            </button>
        </Dropdown.Content>
    </Dropdown>
</td>

まあそうなるとボタンも追加したいってことで

const DropdownButton = ({ className = '', onClick, children }) => {
    return (
        <button
            onClick={onClick}
            className={
                'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out ' +
                className
            }
        >
            {children}
        </button>
    );
};

Dropdown.Button = DropdownButton;

を resources/js/Components/Dropdown.jsx に追加したら

<td className="px-6 py-4 whitespace-nowrap">
    <Dropdown>
        <Dropdown.Trigger>
            <button>
                <VscEllipsis />
            </button>
        </Dropdown.Trigger>
        <Dropdown.Content>
            <Dropdown.Link href={route('users.edit', user.id)}>
                {t("Edit")}
            </Dropdown.Link>
            <Dropdown.Button onClick={() => handleDelete(user.id)}>
                {t("Delete Account")}
            </Dropdown.Button>
        </Dropdown.Content>
    </Dropdown>
</td>

こんな感じにはなる。さらなるコンポーネント化はまあやめとこう

完成したIndex.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
import Dropdown from '@/Components/Dropdown';
import PrimaryButton from '@/Components/PrimaryButton';
import PaginationNav from '@/Components/PaginationNav';
import RoleDisplay from '@/Components/RoleDisplay';
import AppLink from '@/Components/AppLink';
import EmailWithStatus from '@/Components/EmailWithStatus';
import LastLoginDisplay from '@/Components/LastLoginDisplay';
import {
    VscVerifiedFilled,
    VscUnverified,
    VscInfo,
    VscAdd,
    VscEllipsis
} from "react-icons/vsc";


export default function UserIndex({ auth, users }) {
    const { t } = useLaravelReactI18n();
    const dayjs = useConfiguredDayjs();

    const handleDelete = (id) => {
        if (confirm(t("Are you sure you want to delete your account?"))) {
            router.delete(route('users.destroy', id));
        }
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Users")}</h2>}
        >
            <Head title="Users" />

            <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 bg-white border-b border-gray-200 flex justify-end">
                            <PrimaryButton href={route('users.create')}>
                                <VscAdd className="mr-2" />{t("Create New User")}
                            </PrimaryButton>
                        </div>
                        <PaginationNav links={users.links} />

                        <table className="min-w-full divide-y divide-gray-200 m-3 mb-20">
                            <thead className="bg-gray-50">
                                <tr>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("ID")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Email")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Name")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Roles")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Last Login")}
                                    </th>
                                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                        {t("Action")}
                                    </th>
                                </tr>
                            </thead>
                            <tbody className="bg-white divide-y divide-gray-200">
                                {users.data.map((user, index) => (
                                    <tr key={index}>
                                        <td className="px-6 py-4 whitespace-nowrap">
                                            {user.id}
                                        </td>

                                        <td className="px-6 py-4 whitespace-nowrap flex items-center">
                                            <AppLink href={route('users.show', user.id)}>
                                                <EmailWithStatus email={user.email} isVerified={user.email_verified_at !== null} />
                                            </AppLink>
                                        </td>

                                        <td className="px-6 py-4 whitespace-nowrap">
                                            {user.name}
                                        </td>

                                        <td className="px-6 py-4 whitespace-nowrap">
                                            <RoleDisplay roles={user.roles} />
                                        </td>

                                        <td className="px-6 py-4 whitespace-nowrap">
                                            <LastLoginDisplay lastLoginAt={user.last_login_at} />
                                        </td>

                                        <td className="px-6 py-4 whitespace-nowrap">
                                            <Dropdown>
                                                <Dropdown.Trigger>
                                                    <button>
                                                        <VscEllipsis />
                                                    </button>
                                                </Dropdown.Trigger>
                                                <Dropdown.Content>
                                                    <Dropdown.Link href={route('users.edit', user.id)}>
                                                        {t("Edit")}
                                                    </Dropdown.Link>
                                                    <Dropdown.Button onClick={() => handleDelete(user.id)}>
                                                        {t("Delete Account")}
                                                    </Dropdown.Button>
                                                </Dropdown.Content>
                                            </Dropdown>
                                        </td>
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

ん〜unusedなimportがありそうだけどまあいいか

Show.jsx


プロフィールからのコピペだし、まあ何となく直しがいがあるんじゃないかぬ

プロフィール欄がまず必要ない

                        <section>
                            <header>
                                <h2 className="text-lg font-medium text-gray-900">{t("Profile")}</h2>
                                <p className="mt-1 text-sm text-gray-600">
                                    Information about this user.
                                </p>
                            </header>

削除。てかセクション自体削除

email verify

                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Email")}</h3>
                                <div className="mt-2 flex items-center text-lg text-gray-500">
                                    { user.email_verified_at !== null ? (
                                        <VscVerifiedFilled className="text-green-500 mr-2 text-xl" />
                                    ) : (
                                        <VscUnverified className="text-red-500 mr-2 text-lg" />
                                    )}
                                        <span>{user.email}</span>
                                </div>
                            </div>

当然置き換える

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Email")}</h3>
    <div className="mt-2 flex items-center text-lg text-gray-500">
        <EmailWithStatus email={user.email} isVerified={user.email_verified_at !== null} />
    </div>
</div>

Role表示

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Roles")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        {user.roles && user.roles.length > 0 ? (
            user.roles.map((role, index) => (
                <span
                key={index}
                className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
            >
                    {role.name}
            </span>
            ))
        ) : (
            <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-600">
                No roles
            </span>
        )}
    </p>
</div>

置き換え(余計な属性は削除)

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Roles")}</h3>
    <p className="mt-2">
        <RoleDisplay roles={user.roles} />
    </p>
</div>

ラストログインとか

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        {user.last_login_at ? (
            <>
            <span>{dayjs(user.last_login_at).format('YYYY-MM-DD HH:mm:ss')}</span>
            <small className="ml-2 text-sm text-gray-600">({dayjs(user.last_login_at).fromNow()})</small>
            </>
        ) : (
            <span className="text-gray-500">{t("Never")}</span>
        )}
    </p>

    <div>
        <Link href={route('users.activity-log', user.id)} className="text-blue-600 hover:text-blue-900">
            <button className="bg-blue-500 hover:bg-blue-700 text-white py-1 px-2 rounded flex items-center">
                View Activity Log
            </button>
        </Link>
    </div>
</div>
<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        <LastLoginDisplay lastLoginAt={user.last_login_at} />
    </p>

    <div>
        <PrimaryButton href={route('users.activity-log', user.id)}>{t("View Activity Log")}</PrimaryButton>
    </div>
</div>

ざっと変更

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        <LastLoginDisplay lastLoginAt={user.last_login_at} />
    </p>

    <div>
        <PrimaryButton href={route('users.activity-log', user.id)}>
            <VscGraphLine className="mr-2"/> {t("View Activity Log")}
        </PrimaryButton>
    </div>
</div>

アイコンもつけた

とはいえログインないのにアクティビティーログもないだろってことで

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        <LastLoginDisplay lastLoginAt={user.last_login_at} />
    </p>

    <div>
        <PrimaryButton href={route('users.activity-log', user.id)} disabled={!user.last_login_at}>
            <VscGraphLine className="mr-2"/> {t("View Activity Log")}
        </PrimaryButton>
    </div>
</div>

とやっても実は押せてしまうのでコンポーネントを変更

export default function PrimaryButton({ className = '', disabled, children, href, ...props }) {
    const buttonClass = `inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150 ${disabled ? 'opacity-25 cursor-not-allowed' : ''} ` + className;

    const handleClick = (e) => {
        if (disabled) {
            e.preventDefault();
        }
    };

    if (href) {
        return (
            <a
                href={href}
                className={buttonClass}
                onClick={handleClick}
                {...props}
            >
                {children}
            </a>
        );
    }

    return (
        <button
            {...props}
            className={buttonClass}
            disabled={disabled}
        >
            {children}
        </button>
    );
}

aタグのハンドリングとカーソルの変更

でまあlast_login_atが空だからacitivityログが無いともいえないので(セッションを強奪された場合とか、まあその場合はアプリが終わってますが)、一応ちゃんとアクティビティーのログをみるようにした。

<div>
    <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
    <p className="mt-2 text-lg text-gray-500">
        <LastLoginDisplay lastLoginAt={user.last_login_at} />
    </p>

    <div>
        <PrimaryButton
            href={route('users.activity-log', user.id)}
            disabled={!user.activity_logs || user.activity_logs.length === 0}
        >
            <VscGraphLine className="mr-2"/> {t("View Activity Log")}
        </PrimaryButton>
    </div>
</div>

コントローラーのロードを

        $user->load(['roles', 'activities']);

にしておく必要はある。

削除編集リンクボタン

てか冷静に考えるまでもなくShowから編集、削除もできた方がよい。

                        <div>
                            <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
                            <p className="mt-2 text-lg text-gray-500">
                                <LastLoginDisplay lastLoginAt={user.last_login_at} />
                            </p>

                            <div>
                                <PrimaryButton
                                    href={route('users.activity-log', user.id)}
                                    disabled={!user.activities || user.activities.length === 0}
                                >
                                    <VscGraphLine className="mr-2"/> {t("View Activity Log")}
                                </PrimaryButton>
                            </div>
                        </div>

                        <div className="mt-6 flex justify-end space-x-4">
                            <PrimaryButton href={route('users.edit', user.id)}>
                                <VscEdit className="mr-2" /> {t("Edit")}
                            </PrimaryButton>
                            <DangerButton onClick={() => handleDelete(user.id)}>
                                <VscTrash className="mr-2" />
                                {t("Delete Account")}
                            </DangerButton>
                        </div>

完成したShow.jsx

完成したresources/js/Pages/Users/Show.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { VscGraphLine, VscEdit, VscTrash } from "react-icons/vsc";
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';

import RoleDisplay from '@/Components/RoleDisplay';
import EmailWithStatus from '@/Components/EmailWithStatus';
import LastLoginDisplay from '@/Components/LastLoginDisplay';
import PrimaryButton from '@/Components/PrimaryButton';
import DangerButton from '@/Components/DangerButton';

export default function UserShow({ auth, user }) {
    const { t } = useLaravelReactI18n();
    const dayjs = useConfiguredDayjs();

    const handleDelete = (id) => {
        if (confirm(t("Are you sure you want to delete your account?"))) {
            router.delete(route('users.destroy', id));
        }
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("User Details")}</h2>}
        >
            <Head title={t("User Details")} />

            <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 p-4">
                        <div className="mt-6 space-y-6">
                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">ID</h3>
                                <p className="mt-2 text-lg text-gray-500">{user.id}</p>
                            </div>

                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Email")}</h3>
                                <div className="mt-2 flex items-center text-lg text-gray-500">
                                    <EmailWithStatus email={user.email} isVerified={user.email_verified_at !== null} />
                                </div>
                            </div>

                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Name")}</h3>
                                <p className="mt-2 text-lg text-gray-500">{user.name}</p>
                            </div>

                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Roles")}</h3>
                                <p className="mt-2">
                                    <RoleDisplay roles={user.roles} />
                                </p>
                            </div>

                            <div>
                                <h3 className="text-lg font-medium leading-6 text-gray-900">{t("Last Login At")}</h3>
                                <p className="mt-2 text-lg text-gray-500">
                                    <LastLoginDisplay lastLoginAt={user.last_login_at} />
                                </p>

                                <div>
                                    <PrimaryButton
                                        href={route('users.activity-log', user.id)}
                                        disabled={!user.activities || user.activities.length === 0}
                                    >
                                        <VscGraphLine className="mr-2"/> {t("View Activity Log")}
                                    </PrimaryButton>
                                </div>
                            </div>

                            <div className="mt-6 flex justify-end space-x-4">
                                <PrimaryButton href={route('users.edit', user.id)}>
                                    <VscEdit className="mr-2" /> {t("Edit")}
                                </PrimaryButton>
                                <DangerButton onClick={() => handleDelete(user.id)}>
                                    <VscTrash className="mr-2" />
                                    {t("Delete Account")}
                                </DangerButton>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

ActivityLog.jsx

これは割と適当な出来だったりしたのでいろいろ直している
まあページャーは少なくとも

 <PaginationNav links={activities.links} />

これで済むだろう。あとは適当に翻訳を入れといた

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
import PaginationNav from '@/Components/PaginationNav';

export default function ActivityLog({ auth, user, activities }) {
    const { t } = useLaravelReactI18n();
    const dayjs = useConfiguredDayjs();

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("View Activity Log")}</h2>}
        >
            <Head title={t("View Activity Log")} />

            <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 p-4">
                        <div className="mt-6">
                            <PaginationNav links={activities.links} />

                            {activities.data && activities.data.length > 0 ? (
                                <table className="min-w-full divide-y divide-gray-200">
                                    <thead className="bg-gray-50">
                                        <tr>
                                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
                                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t("Description")}</th>
                                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t("Created At")}</th>
                                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t("Properties")}</th>
                                        </tr>
                                    </thead>
                                    <tbody className="bg-white divide-y divide-gray-200">
                                        {activities.data.map((activity, index) => (
                                            <tr key={index}>
                                                <td className="px-6 py-4 whitespace-nowrap">{activity.id}</td>
                                                <td className="px-6 py-4 whitespace-nowrap">{activity.description}</td>
                                                <td className="px-6 py-4 whitespace-nowrap">
                                                    <span>{dayjs(activity.created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
                                                    <small className="ml-2 text-sm text-gray-600">({dayjs(activity.created_at).fromNow()})</small>
                                                </td>
                                                <td className="px-6 py-4 whitespace-nowrap">
                                                    <table className="min-w-full divide-y divide-gray-200">
                                                        <tbody className="bg-white divide-y divide-gray-200">
                                                            {Object.entries(activity.properties).map(([key, value], idx) => (
                                                                <tr key={idx}>
                                                                    <th className="px-2 py-1 text-sm text-gray-500">{key}</th>
                                                                    <td className="px-2 py-1 text-sm text-gray-900">{value}</td>
                                                                </tr>
                                                            ))}
                                                        </tbody>
                                                    </table>
                                                </td>
                                            </tr>
                                        ))}
                                    </tbody>
                                </table>
                            ) : (
                                <p className="text-gray-500">{t("No activities recorded.")}</p>
                            )}
                        </div>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

smデバイスの確認…

は長くなりすぎたしこりゃこのページじゃ無理やわ(笑)次回

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