実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その40)
0. 初めに
こんにちは!
実務1年目駆け出しエンジニアによるWebアプリケーション開発解説シリーズです。
かなり終わりが見えてきました、フロントエンド編ですが、今日は退会ページを作成したいと思います。
今日からGWということで連日投稿に挑戦したいと思います...!!
1. 退会ページ作成
現在、マイページにて「退会」ボタンがありますが、クリックても何も起こりません。
退会ページが開かれるように、ページコンポーネントを作成しましょう。

1.1 ブランチ運用
いつも通り、developブランチを最新化させて、新規ブランチを切って作業します。
ブランチ名は、feature/frontend/withdrawal-pageとかにしましょう。
1.2 ルーティング追加
authミドルウェアのマイページ関連あたりに追加です。
routes/web.php
クリックでコードを見る
// マイページ関連
Route::get('/mypage', [MyPageController::class, 'showUser'])->name('mypage.index');
Route::get('/mypage/edit', [MyPageController::class, 'editUser'])->name('mypage.edit');
Route::put('/mypage', [MyPageController::class, 'updateUser'])->name('mypage.update');
Route::delete('/mypage', [MyPageController::class, 'deleteUser'])->name('mypage.delete');
Route::get('/mypage/bookmarks', [MyPageController::class, 'showBookmarks'])->name('mypage.bookmarks');
Route::get('/mypage/withdrawal', [MyPageController::class, 'showWithdrawal'])->name('mypage.withdrawal'); // 追加
Route::delete('/mypage/bookmarks/{bookmark}', [MyPageController::class, 'removeBookmark'])->name('mypage.bookmarks.remove');
1.3 コントローラーメソッド追加
マイページコントローラーにメソッドを新規追加です。
deleteUserの上あたりが良いかと思います。
app/Http/Controllers/MyPageController.php
クリックでコードを見る
/**
* 退会用ページを表示する
*/
public function showWithdrawal(): Response
{
return Inertia::render('MyPage/Withdrawal');
}
public function deleteUser()
{
}
1.4 ページコンポーネント作成
resources/js/Pages/MyPage/Withdrawal.jsx
クリックでコードを見る
import AppLayout from "@/Layouts/AppLayout";
import { Head, useForm, Link } from "@inertiajs/react";
import AlertModal from "@/Components/Common/AlertModal";
import { useState } from "react";
/**
* 退会ページコンポーネント
* @returns {JSX.Element}
*/
const Withdrawal = () => {
const [isConfirmOpen, setConfirmOpen] = useState(false);
const { delete: destroy, processing } = useForm();
const handleWithdrawal = () => {
destroy(route("mypage.delete"), {
onSuccess: () => setConfirmOpen(false),
});
};
return (
<AppLayout title="退会">
<Head title="退会" />
<div className="mt-2">
<div className="flex items-center justify-between border-b border-black pb-2 w-full">
<h2 className="text-xl font-bold text-black">退会</h2>
</div>
<div className="mt-6 space-y-4 text-[#747D8C]">
<p>
退会すると、以下のデータがすべて削除されます。この操作は取り消せません。
</p>
<ul className="list-disc list-inside space-y-1">
<li>アカウント情報(ニックネーム・メールアドレス)</li>
<li>ブックマーク</li>
<li>投稿したレビュー・コメント</li>
</ul>
<p className="font-semibold text-[#FF0000]">
本当に退会しますか?
</p>
</div>
<div className="mt-8 flex items-center gap-4">
<button
type="button"
onClick={() => setConfirmOpen(true)}
className="px-6 py-2 bg-[#FF0000] text-white font-bold rounded-md hover:opacity-80 transition cursor-pointer"
>
退会する
</button>
<Link
href={route("mypage.index")}
className="px-6 py-2 bg-[#EEF7FB] text-[#747D8C] shadow-md font-bold rounded-md hover:shadow-lg transition-shadow"
>
マイページに戻る
</Link>
</div>
</div>
<AlertModal
isOpen={isConfirmOpen}
onClose={() => setConfirmOpen(false)}
title="退会の確認"
message="退会すると元に戻せません。本当に退会しますか?"
actionLabel="退会する"
onAction={handleWithdrawal}
cancelLabel="キャンセル"
isProcessing={processing}
/>
</AppLayout>
);
};
export default Withdrawal;
1.5 マイページ修正
resources/js/Pages/MyPage/Index.jsx
クリックでコードを見る
{/* 退会 */}
<div className="mt-6 mb-4 flex items-center">
<button
type="button"
onClick={() => router.get(route('mypage.withdrawal'))}
className="text-lg font-semibold text-[#747D8C] hover:underline cursor-pointer text-left"
>
退会
</button>
</div>
1.6 動作確認
適当なアカウントでログインして、マイページから退会ページが開くかを確認してください。
「退会する」ボタンをクリックすると、アラートモーダルが開きます。

ここまでできたら、コミット・プッシュ、PR作成・マージを忘れずに行いましょう!
2. マイページ関連未実装部分実装
マイページに関して実装を後回しにしていた部分がいくつかあったので、それらを実装します。
- 作成済み大学の表示
- 作成済み学部の表示
- 作成済み研究室の表示
- ユーザー情報の更新
2.1 ブランチ運用
例によって、developブランチを最新化させて、そこから新規にfeature/frontend/mypage-not-implementedなどの名前で新規ブランチを切って作業します。
2.2 作成済み大学・学部・研究室表示
自分が作成した大学・学部・研究室の一覧を見られるようにします。
マイページコントローラー修正
app/Http/Controllers/MyPageController.php
クリックでコードを見る
use App\Models\University; // 追加
use App\Models\Faculty; // 追加
use App\Models\Lab; // 追加
class MyPageController extends Controller
{
/**
* ユーザー情報を表示する
*/
public function showUser(): Response // 追加
{
$user = Auth::user();
// 管理者・一般ユーザー問わず通知を取得
$notifications = $user->notifications()->latest()->get();
// ブックマーク済み研究室とそのレビュー情報・総合評価を取得
$bookmarks = $user->bookmarks()->with(['lab.reviews'])->get()->map(function($bookmark) {
$lab = $bookmark->lab;
if (!$lab) return null;
// 評価項目
$ratingColumns = [
'mentorship_style',
'lab_atmosphere',
'achievement_activity',
'constraint_level',
'facility_quality',
'work_style',
'student_balance',
];
// 各項目の平均値
$averagePerItem = collect($ratingColumns)->mapWithKeys(function ($column) use ($lab) {
return [$column => $lab->reviews->avg($column)];
});
// 総合評価値
$overallAverage = $averagePerItem->avg();
$lab->overall_avg = $overallAverage;
foreach ($ratingColumns as $column) {
$lab->{"avg_{$column}"} = $averagePerItem[$column];
}
// reviews_countも追加
$lab->reviews_count = $lab->reviews->count();
// 大学・学部名も追加
$lab->load(['faculty.university']);
return $lab;
})->filter();
// ユーザーが作成した大学・学部・研究室を取得
$universities = University::where('created_by', $user->id)->get();
$faculties = Faculty::where('created_by', $user->id)->with('university')->get();
$createdLabs = Lab::where('created_by', $user->id)->with(['faculty.university', 'reviews'])->get()->map(function ($lab) {
$ratingColumns = [
'mentorship_style', 'lab_atmosphere', 'achievement_activity',
'constraint_level', 'facility_quality', 'work_style', 'student_balance',
];
$averagePerItem = collect($ratingColumns)->mapWithKeys(fn($col) => [$col => $lab->reviews->avg($col)]);
$lab->overall_avg = $averagePerItem->avg();
foreach ($ratingColumns as $col) {
$lab->{"avg_{$col}"} = $averagePerItem[$col];
}
$lab->reviews_count = $lab->reviews->count();
return $lab;
});
return Inertia::render('MyPage/Index', [
'title' => "{$user->name}さんのマイページ",
'user' => $user,
'notifications' => $notifications,
'bookmarks' => $bookmarks,
'universities' => $universities,
'faculties' => $faculties,
'createdLabs' => $createdLabs,
]);
}
マイページコンポーネント修正
resources/js/Pages/MyPage/Index.jsx
クリックでコードを見る
import UniversityCard from "@/Components/University/UniversityCard"; // 追加
import FacultyCard from "@/Components/Faculty/FacultyCard"; // 追加
/**
* マイページのトップコンポーネント
* @param {Object} props - コンポーネントのprops
* @param {string} props.title - ページタイトル
* @param {Object} props.user - ユーザー情報オブジェクト
* @param {Array} [props.bookmarks=[]] - ブックマークされた研究室のリスト
* @param {Array} [props.notifications=[]] - 通知オブジェクトの配列
* @returns {JSX.Element} コンポーネントのJSX
*/
const Index = ({ title, user, bookmarks = [], notifications = [], universities = [], faculties = [], createdLabs = [] }) => {
// 大学作成モーダルの開閉状態
const [isUniversityModalOpen, setUniversityModalOpen] = useState(false);
// 横スクロール用インデックス
const [currentIndex, setCurrentIndex] = useState(0);
// 大学カルーセル用インデックス
const [uniIndex, setUniIndex] = useState(0);
// 学部カルーセル用インデックス
const [facIndex, setFacIndex] = useState(0);
// 研究室カルーセル用インデックス
const [labIndex, setLabIndex] = useState(0);
const handlePrev = () => {
setCurrentIndex((prev) => Math.max(prev - 1, 0));
};
const handleNext = () => {
setCurrentIndex((prev) => Math.min(prev + 1, bookmarks.length - 1));
};
const handleUniPrev = () => {
setUniIndex((prev) => Math.max(prev - 1, 0));
};
const handleUniNext = () => {
setUniIndex((prev) => Math.min(prev + 1, universities.length - 1));
};
const handleFacPrev = () => {
setFacIndex((prev) => Math.max(prev - 1, 0));
};
const handleFacNext = () => {
setFacIndex((prev) => Math.min(prev + 1, faculties.length - 1));
};
const handleLabPrev = () => {
setLabIndex((prev) => Math.max(prev - 1, 0));
};
const handleLabNext = () => {
setLabIndex((prev) => Math.min(prev + 1, createdLabs.length - 1));
};
// 横スクロールもサポート(スワイプやホイール)
const handleScroll = (e) => {
if (bookmarks.length <= 1) return;
if (e.deltaX > 10 || e.deltaY > 10) {
handleNext();
} else if (e.deltaX < -10 || e.deltaY < -10) {
handlePrev();
}
};
// タッチスワイプ対応
const touchStartX = useRef(0);
const handleTouchStart = (e) => {
touchStartX.current = e.touches[0].clientX;
};
const handleTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (touchEndX - touchStartX.current > 50) {
handlePrev();
} else if (touchEndX - touchStartX.current < -50) {
handleNext();
}
};
// 大学カルーセル用タッチスワイプ
const uniTouchStartX = useRef(0);
const handleUniTouchStart = (e) => {
uniTouchStartX.current = e.touches[0].clientX;
};
const handleUniTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (touchEndX - uniTouchStartX.current > 50) {
handleUniPrev();
} else if (touchEndX - uniTouchStartX.current < -50) {
handleUniNext();
}
};
// 学部カルーセル用タッチスワイプ
const facTouchStartX = useRef(0);
const handleFacTouchStart = (e) => {
facTouchStartX.current = e.touches[0].clientX;
};
const handleFacTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (touchEndX - facTouchStartX.current > 50) {
handleFacPrev();
} else if (touchEndX - facTouchStartX.current < -50) {
handleFacNext();
}
};
// 研究室カルーセル用タッチスワイプ
const labTouchStartX = useRef(0);
const handleLabTouchStart = (e) => {
labTouchStartX.current = e.touches[0].clientX;
};
const handleLabTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (touchEndX - labTouchStartX.current > 50) {
handleLabPrev();
} else if (touchEndX - labTouchStartX.current < -50) {
handleLabNext();
}
};
// LabCardを1件だけ表示
const currentLab = bookmarks[currentIndex];
const currentUniversity = universities[uniIndex];
const currentFaculty = faculties[facIndex];
const currentCreatedLab = createdLabs[labIndex];
// 大学カルーセル用ホイールスクロール
const handleUniScroll = (e) => {
if (universities.length <= 1) return;
if (e.deltaX > 10 || e.deltaY > 10) {
handleUniNext();
} else if (e.deltaX < -10 || e.deltaY < -10) {
handleUniPrev();
}
};
// 学部カルーセル用ホイールスクロール
const handleFacScroll = (e) => {
if (faculties.length <= 1) return;
if (e.deltaX > 10 || e.deltaY > 10) {
handleFacNext();
} else if (e.deltaX < -10 || e.deltaY < -10) {
handleFacPrev();
}
};
// 研究室カルーセル用ホイールスクロール
const handleLabScroll = (e) => {
if (createdLabs.length <= 1) return;
if (e.deltaX > 10 || e.deltaY > 10) {
handleLabNext();
} else if (e.deltaX < -10 || e.deltaY < -10) {
handleLabPrev();
}
};
// ヘッダー右側に表示する通知ドロップダウン
const notificationHeaderIcon = (
<NotificationDropdown notifications={notifications} />
);
return (
<AppLayout title={title} headerRight={notificationHeaderIcon}>
<Head title={title} />
<div className="mt-2">
{/* 基本情報 */}
<div className="flex items-center justify-between border-b border-black pb-2 w-full">
<h2 className="text-xl font-bold text-black">基本情報</h2>
<span className="text-sm text-[#747D8C]">
利用開始日: {user?.created_at ? new Date(user.created_at).toLocaleDateString('ja-JP') : ''}
</span>
</div>
{/* ニックネーム */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">ニックネーム</h3>
<div className="w-96">
<UserInfoBar value={user?.name} onOpenEditModal={() => {}} />
</div>
</div>
{/* e-Mailアドレス */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">e-Mailアドレス</h3>
<div className="w-96">
<UserInfoBar value={user?.email} onOpenEditModal={() => {}} />
</div>
</div>
{/* パスワード */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">パスワード</h3>
<div className="w-96">
<UserInfoBar value="••••••••" onOpenEditModal={() => {}} />
</div>
</div>
{/* 退会 */}
<div className="mt-6 mb-4 flex items-center">
<button
type="button"
onClick={() => router.get(route('mypage.withdrawal'))}
className="text-lg font-semibold text-[#747D8C] hover:underline cursor-pointer text-left"
>
退会
</button>
</div>
{/* ブックマーク済み研究室 */}
<div className="flex items-center justify-between border-b border-black pb-2 w-full mt-8">
<h2 className="text-xl font-bold text-black">ブックマーク済み研究室</h2>
<span className="text-sm text-[#747D8C]">保存済み: {bookmarks.length}件</span>
</div>
<div className="mt-4 flex flex-col items-center">
{bookmarks.length === 0 ? (
<p className="text-[#747D8C]">ブックマーク済みの研究室はありません。</p>
) : (
<div
className="flex items-center w-full max-w-2xl"
onWheel={handleScroll}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* ≪ボタン */}
<button
type="button"
onClick={handlePrev}
disabled={currentIndex === 0}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${currentIndex === 0 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≪
</button>
<div className="flex-1 flex flex-col items-center">
{/* 大学・学部名の表示 */}
<div className="mb-2 text-lg font-semibold text-[#747D8C] text-center">
{currentLab.faculty?.university?.name} {currentLab.faculty?.name}
</div>
<div className="flex justify-center w-full">
<div className="max-w-xl w-full">
<LabCard lab={currentLab} />
</div>
</div>
<div className="mt-2 text-sm text-[#747D8C] text-center">
{currentIndex + 1} / {bookmarks.length}
</div>
</div>
{/* ≫ボタン */}
<button
type="button"
onClick={handleNext}
disabled={currentIndex === bookmarks.length - 1}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${currentIndex === bookmarks.length - 1 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≫
</button>
</div>
)}
</div>
{/* 作成済み大学 */}
<div className="flex items-center justify-between border-b border-black pb-2 w-full mt-8">
<h2 className="text-xl font-bold text-black">作成済み大学</h2>
<span className="text-sm text-[#747D8C]">
作成件数: {universities.length}件
</span>
</div>
<div className="mt-4 flex flex-col items-center">
{universities.length === 0 ? (
<p className="text-[#747D8C]">作成済みの大学はありません。</p>
) : (
<div
className="flex items-center w-full max-w-2xl"
onWheel={handleUniScroll}
onTouchStart={handleUniTouchStart}
onTouchEnd={handleUniTouchEnd}
>
{/* ≪ボタン */}
<button
type="button"
onClick={handleUniPrev}
disabled={uniIndex === 0}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${uniIndex === 0 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≪
</button>
<div className="flex-1 flex flex-col items-center">
<div className="mb-2 text-lg font-semibold text-[#747D8C] text-center">
{currentUniversity.name}
</div>
<div className="flex justify-center w-full">
<div className="max-w-xl w-full">
<UniversityCard university={currentUniversity} />
</div>
</div>
<div className="mt-2 text-sm text-[#747D8C] text-center">
{uniIndex + 1} / {universities.length}
</div>
</div>
{/* ≫ボタン */}
<button
type="button"
onClick={handleUniNext}
disabled={uniIndex === universities.length - 1}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${uniIndex === universities.length - 1 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≫
</button>
</div>
)}
<button
type="button"
onClick={() => setUniversityModalOpen(true)}
>
追加
</button>
</div>
{/* 大学作成モーダル */}
<CreateUniversityModal isOpen={isUniversityModalOpen} onClose={() => setUniversityModalOpen(false)} />
{/* 作成済み学部 */}
<div className="flex items-center justify-between border-b border-black pb-2 w-full mt-8">
<h2 className="text-xl font-bold text-black">作成済み学部</h2>
<span className="text-sm text-[#747D8C]">
作成件数: {faculties.length}件
</span>
</div>
<div className="mt-4 flex flex-col items-center">
{faculties.length === 0 ? (
<p className="text-[#747D8C]">作成済みの学部はありません。</p>
) : (
<div
className="flex items-center w-full max-w-2xl"
onWheel={handleFacScroll}
onTouchStart={handleFacTouchStart}
onTouchEnd={handleFacTouchEnd}
>
<button
type="button"
onClick={handleFacPrev}
disabled={facIndex === 0}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${facIndex === 0 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≪
</button>
<div className="flex-1 flex flex-col items-center">
<div className="mb-2 text-lg font-semibold text-[#747D8C] text-center">
{currentFaculty.university?.name}
</div>
<div className="flex justify-center w-full">
<div className="max-w-xl w-full flex justify-center">
<FacultyCard faculty={currentFaculty} />
</div>
</div>
<div className="mt-2 text-sm text-[#747D8C] text-center">
{facIndex + 1} / {faculties.length}
</div>
</div>
<button
type="button"
onClick={handleFacNext}
disabled={facIndex === faculties.length - 1}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${facIndex === faculties.length - 1 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≫
</button>
</div>
)}
</div>
{/* 作成済み研究室 */}
<div className="flex items-center justify-between border-b border-black pb-2 w-full mt-8">
<h2 className="text-xl font-bold text-black">作成済み研究室</h2>
<span className="text-sm text-[#747D8C]">
作成件数: {createdLabs.length}件
</span>
</div>
<div className="mt-4 flex flex-col items-center">
{createdLabs.length === 0 ? (
<p className="text-[#747D8C]">作成済みの研究室はありません。</p>
) : (
<div
className="flex items-center w-full max-w-2xl"
onWheel={handleLabScroll}
onTouchStart={handleLabTouchStart}
onTouchEnd={handleLabTouchEnd}
>
<button
type="button"
onClick={handleLabPrev}
disabled={labIndex === 0}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${labIndex === 0 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≪
</button>
<div className="flex-1 flex flex-col items-center">
<div className="mb-2 text-lg font-semibold text-[#747D8C] text-center">
{currentCreatedLab.faculty?.university?.name} {currentCreatedLab.faculty?.name}
</div>
<div className="flex justify-center w-full">
<div className="max-w-xl w-full">
<LabCard lab={currentCreatedLab} />
</div>
</div>
<div className="mt-2 text-sm text-[#747D8C] text-center">
{labIndex + 1} / {createdLabs.length}
</div>
</div>
<button
type="button"
onClick={handleLabNext}
disabled={labIndex === createdLabs.length - 1}
className={`text-2xl px-2 text-[#747D8C] hover:text-gray-600 transition ${labIndex === createdLabs.length - 1 ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
>
≫
</button>
</div>
)}
</div>
</div>
</AppLayout>
);
};
export default Index;
動作確認
適当なアカウントでログインして、作成後マイページで以下のように表示されるようになればOKです!

問題なさそうでしたら、コミットしておきましょう。
2.3 ユーザー情報更新
ユーザー情報を更新できるようにします。
マイページコントローラー修正
app/Http/Controllers/MyPageController.php
クリックでコードを見る
// ediUser()は削除
/**
* ユーザー情報を更新
*/
public function updateUser(Request $request): RedirectResponse
{
$rules = [
'nickname' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email,' . Auth::id(),
];
if ($request->filled('password')) {
$rules['password'] = 'required|string|min:8|confirmed';
}
$request->validate($rules);
/** @var User $user */
$user = Auth::user();
$user->name = $request->nickname;
$user->email = $request->email;
if ($request->filled('password')) {
$user->password = Hash::make($request->password);
}
$user->save();
return redirect()->route('mypage.index')->with('success', 'ユーザー情報を更新しました');
}
ユーザー情報の更新は、モーダルで行うようにしたいので、ページを表示するためのメソッドであるeditUser()は不要になるので削除しました。
ユーザー情報編集用モーダルコンポーネント作成
モーダル用に新規ファイルを作成します。
resources/js/Components/MyPage/EditUserModal.jsx
クリックでコードを見る
import { useForm } from '@inertiajs/react';
import { useEffect } from 'react';
import Modal from '../Common/Modal';
import UserSubmitButton from './UserSubmitButton';
import InputField from '../Common/InputField';
/**
* ユーザー情報編集モーダル
* @param {Object} props
* @param {boolean} props.isOpen - モーダルの開閉状態
* @param {Function} props.onClose - モーダルを閉じる
* @param {Object} props.user - 編集対象のユーザーオブジェクト
* @returns {JSX.Element} コンポーネントのJSX
*/
const EditUserModal = ({ isOpen, onClose, user }) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="ユーザー情報を編集する"
size='sm'
>
<div className="h-[400px] flex flex-col">
{/* フォーム領域 */}
<div className="flex-1">
<EditUserForm onClose={onClose} user={user} />
</div>
</div>
</Modal>
);
};
/**
* ユーザー情報編集フォーム
* @param {Object} props
* @param {Function} props.onClose - モーダルを閉じる
* @param {Object} props.user - 編集対象のユーザーオブジェクト
* @returns {JSX.Element} コンポーネントのJSX
*/
const EditUserForm = ({ onClose, user }) => {
const { data, setData, put, processing, errors, reset } = useForm({
nickname: user?.name ?? '',
email: user?.email ?? '',
password: '',
password_confirmation: '',
});
useEffect(() => {
reset();
setData({
nickname: user?.name ?? '',
email: user?.email ?? '',
password: '',
password_confirmation: '',
});
}, [user]);
const submit = e => {
e.preventDefault();
put(route('mypage.update'), {
onSuccess: () => {
reset();
onClose();
},
preserveScroll: true,
});
};
return (
<form onSubmit={submit} className="h-full flex flex-col">
<div className="flex-1">
{/* ニックネーム */}
<ErrorSlot message={errors.nickname} />
<InputField
type="text"
value={data.nickname}
onChange={e => setData('nickname', e.target.value)}
placeholder="ニックネーム"
size="sm"
className="mb-2 w-full"
/>
{/* メールアドレス */}
<ErrorSlot message={errors.email} />
<InputField
type="email"
value={data.email}
onChange={e => setData('email', e.target.value)}
placeholder="メールアドレス"
size="sm"
className="mb-2 w-full"
/>
{/* 新しいパスワード */}
<ErrorSlot message={errors.password} />
<InputField
type="password"
value={data.password}
onChange={e => setData('password', e.target.value)}
placeholder="新しいパスワード(変更する場合のみ)"
size="sm"
className="mb-2 w-full"
/>
{/* パスワード確認 */}
<ErrorSlot message={errors.password_confirmation} />
<InputField
type="password"
value={data.password_confirmation}
onChange={e => setData('password_confirmation', e.target.value)}
placeholder="新しいパスワード(確認用)"
size="sm"
className="mb-2 w-full"
/>
</div>
{/* 送信ボタン */}
<div className="mt-6 flex justify-center">
<UserSubmitButton disabled={processing} />
</div>
</form>
);
};
const ErrorSlot = ({ message }) => (
<p className="h-5 text-sm leading-5 text-red-500 overflow-hidden">
{message ?? '\u00A0'}
</p>
);
export default EditUserModal;
また、このコンポーネント内で使われている送信ボタン用のコンポーネントも作りましょう。
resources/js/Components/MyPage/UserSubmitButton.jsx
クリックでコードを見る
/**
* ユーザー情報更新ボタンコンポーネント
* @param {Object} props
* @param {boolean} [props.disabled=false] - 無効状態
* @param {string} [props.type='submit'] - ボタンタイプ
* @returns {JSX.Element} コンポーネントのJSX
*/
const UserSubmitButton = ({ disabled = false, type = 'submit' }) => {
return (
<button
type={type}
disabled={disabled}
className={`
p-1 bg-[#33E1ED] shadow-md rounded-md
hover:shadow-lg transition-shadow
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span className="px-14 py-1 block rounded text-white font-bold">
更新する
</span>
</button>
);
};
export default UserSubmitButton;
マイページコンポーネント修正
これらを呼び出せるように修正しましょう。
resources/js/Pages/MyPage/Index.jsx
クリックでコードを見る
import EditUserModal from "@/Components/MyPage/EditUserModal"; // 追加
const Index = ({ title, user, bookmarks = [], notifications = [], universities = [], faculties = [], createdLabs = [] }) => {
// 大学作成モーダルの開閉状態
const [isUniversityModalOpen, setUniversityModalOpen] = useState(false);
// 追加: ユーザー情報編集モーダルの開閉状態
const [isUserEditModalOpen, setUserEditModalOpen] = useState(false);
// ...
retrun (
{/* ニックネーム */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">ニックネーム</h3>
<div className="w-96">
<UserInfoBar value={user?.name} onOpenEditDialog={() => setUserEditModalOpen(true)} />
</div>
</div>
{/* e-Mailアドレス */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">e-Mailアドレス</h3>
<div className="w-96">
<UserInfoBar value={user?.email} onOpenEditDialog={() => setUserEditModalOpen(true)} />
</div>
</div>
{/* パスワード */}
<div className="mt-6 mb-4 flex items-center">
<h3 className="text-lg font-semibold text-[#747D8C] w-40 shrink-0">パスワード</h3>
<div className="w-96">
<UserInfoBar value="••••••••" onOpenEditDialog={() => setUserEditModalOpen(true)} />
</div>
</div>
)
また、もともとあった編集用のページコンポーネントは不要になったので削除します。
- 削除:
resources/js/Pages/MyPage/Edit.jsx
さらに不要になったルーティングも削除しておきましょう。
routes/web.php
クリックでコードを見る
Route::get('/mypage/edit', [MyPageController::class, 'editUser'])->name('mypage.edit'); // 削除
動作確認
バリデーションがはたらくことと更新ができることを確認できたら、コミット・プッシュ、PR作成、マージをお忘れなく!
3. まとめ・次回予告
今日は、退会ページを作り、さらにマイページの未実装部分も実装しました!
次回はファビコンを作成する予定です!
お楽しみに。
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
- その29: フロントエンド実装編⑧前編 ~研究室詳細・レビュー画面作成前編~
- その29.5: フロントエンド実装編⑧後編 ~研究室詳細・レビュー画面作成後編~
- その30: フロントエンド実装編⑨ ~マイページ作成~
- その31: フロントエンド実装編⑩ ~パンくずリスト作成~
- その32: フロントエンド実装編⑪ ~ログイン・新規登録モーダル作成~
- その33: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
- その34: フロントエンド実装編⑬ ~学部作成・編集モーダル作成~
- その35: フロントエンド実装編⑭ ~研究室作成・編集モーダル作成~
- その36: フロントエンド実装編⑮ ~レビュー作成・編集モーダル作成~
- その37: フロントエンド実装編⑯ ~コメント一覧表示モーダル作成~
- その38: フロントエンド実装編⑰ ~編集履歴ページ・削除依頼ページ・通知ドロップダウン作成~
- その39: フロントエンド実装編⑱ ~トースト作成~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"



