実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その29)
0. 初めに
こんにちは!
見習いエンジニアの僕がWebアプリケーションをゼロから開発するシリーズです。
今日は、ついにアプリの目玉となる研究室詳細・レビュー画面を作成していきたいと思います!
1. ブランチ運用
いつも通り、developブランチを最新化して、新規ブランチfeature/frontend/lab-detail-pageを切って作業します。
こまめなコミットを心がけましょう。
2. 画面デザイン
こちらも例によって、完成イメージを共有してから実装に臨みましょう。

3. コントローラー修正
必要な情報が一部足りていなかったので追加しておきました。
public function show(Lab $lab, Request $request)
{
// 大学・学部、レビューのデータも一緒に渡す
// universityはfacultyを経由して取得
$lab->load(['faculty.university', 'reviews']);
// コメントデータを取得(投稿者情報も含む)
$comments = $lab->comments()->with('user')->latest()->get();
// 平均値を計算するために、評価項目のカラム名を定義
$ratingColumns = [
'mentorship_style',
'lab_atmosphere',
'achievement_activity',
'constraint_level',
'facility_quality',
'work_style',
'student_balance',
];
// 1. 各評価項目のユーザー間の平均値 (Average per Item) を計算
$averagePerItem = collect($ratingColumns)->mapWithKeys(function ($column) use ($lab) {
// 各評価項目の平均を計算(全レビューを対象)
return [$column => $lab->reviews->avg($column)];
});
// 2. 新しい「総合評価」:各項目の平均値のさらに平均を計算
// $averagePerItem の値(平均点)をコレクションとして取り出し、その平均を求める
$overallAverage = $averagePerItem->avg();
// 3. 現在のユーザーのレビューを取得
$userReview = null;
$userOverallAverage = null;
if (Auth::check()) {
$userReview = $lab->reviews->where('user_id', Auth::id())->first();
// ユーザーのレビューが存在する場合、個別の総合評価を計算
if ($userReview) {
$userRatings = collect($ratingColumns)->map(function ($column) use ($userReview) {
return $userReview->$column;
})->filter(function ($value) {
return $value !== null;
});
$userOverallAverage = $userRatings->avg();
}
}
// ユーザーのブックマーク状態を取得
$userBookmark = $lab->bookmarks()->where('user_id', Auth::id())->first();
$bookmarkCount = $lab->bookmarks()->count();
// 検索クエリを取得
$searchQuery = $request->input('query', '');
// 研究室のデータに加えて、求めたレビューの平均値とユーザーのレビュー、コメント、ブックマーク、認証情報も一緒に渡す
return Inertia::render('Lab/Show', [
'lab' => $lab,
'overallAverage' => $overallAverage,
'averagePerItem' => $averagePerItem,
'userReview' => $userReview,
'userOverallAverage' => $userOverallAverage,
'userBookmark' => $userBookmark,
'bookmarkCount' => $bookmarkCount,
'query' => $searchQuery,
'ratingData' => [
'columns' => $ratingColumns,
],
'comments' => $comments,
'auth' => [
'user' => Auth::user(),
],
]);
}
4. レーダーチャート作成(UI作成)
評価指標の可視化には、レーダーチャートを採用したいと思います。
Chart.jsを導入する
レーダーチャートなどのグラフを作成するために便利な、Chart.jsを今回は使ってみたいと思います。
Dockerコンテナの中で以下のコマンドを実行して、インストールしましょう。
実行コマンド
$ npm install chart.js react-chartjs-2
Lab/Show.jsxを作成する
バックエンド編で既に作成済みのファイルですが、中身をすべて消して一から作りなおしましょう。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex justify-center">
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
内容を解説していきますね。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
必要なモジュールをインポートしています。
例によって、コンテンツをAppLayoutで囲みます。
Chart.jsでは、初期設定で必要な機能を明示的に登録する必要があります。
ChartJS.register(
RadialLinearScale, // レーダーチャートの放射状スケール
PointElement, // データポイント(点)
LineElement, // 線
Filler, // 塗りつぶし
Tooltip, // ホバー時のツールチップ
Legend // 凡例
);
オブジェクト型のaveragePerItemをUI側で扱いやすくするために配列に変換しています。
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
念のため、||を用いてnullやundefinedの場合に0を表示するようにしています。
レーダーチャートの見た目や動作を細かく制御する設定オブジェクトを定義します。
const chartOptions = {
responsive: true, // レスポンシブ対応
maintainAspectRatio: true, // アスペクト比を維持
scales: { ... }, // 軸の設定
plugins: { ... }, // プラグインの設定
};
scales.rは放射状のスケールについて設定できるプロパティです。
scales: {
r: { // "r"はradial(放射状)の意味
angleLines: {
display: true, // 中心から各頂点への線を表示
},
suggestedMin: 0, // 最小値
suggestedMax: 5, // 最大値
ticks: {
stepSize: 1, // 目盛りの間隔(0,1,2,3,4,5)
font: {
size: 12, // 目盛り数字のフォントサイズ
},
},
pointLabels: {
font: {
size: 14, // 各頂点のラベル("指導スタイル"など)のフォントサイズ
},
},
},
},
プラグインの設定もできます。
plugins: {
legend: {
position: "top", // 凡例をチャート上部に配置
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
${context.dataset.label}は、凡例のラベルを、${context.raw.toFixed(2)}は評価値の小数点以下2桁分を表します。
実際に使うときは、別途react-chartjs-2からインポートしたRadarを使います。
propsに先ほど定義したデータとオプション設定を渡します。
<Radar data={chartData} options={chartOptions} />
react-chartjs-2は、Chart.jsの機能をReactで使いやすくしてくれたライブラリです。
5. 研究室情報表示部分作成(UI作成)
次に、研究室情報を表示します。
2列構成にする
グリッドレイアウトを採用して、2列構成にしたいと思います。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 研究室概要 */}
<div>
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
この前後にいろいろと情報をつけ足していきましょう。
総合評価を追加する
前回作成した、Lab/Indexと同様にStarRatingを使用して、総合評価値を視覚化します。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import StarRating from "@/Components/Lab/Star/StarRating";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem, overallAverage }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 総合評価と研究室概要 */}
<div>
{/* 総合評価 */}
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価
</h2>
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex items-center gap-2">
<StarRating rating={overallAverage || 0} />
<span className="text-sm text-[#F4BB42]">
{overallAverage ? Number(overallAverage).toFixed(2) : "0.00"}
</span>
</div>
) : (
<p className="text-sm text-gray-500">まだ評価がありません</p>
)}
</div>
{/* 研究室概要 */}
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
フォーマット用の関数を切り出す
先ほども作った、小数点以下2桁をフォーマットする処理ですが、内容が前回作ったLabCardと共通しているので、関数として外部に切り出して、インポートして使えるように修正したいと思います。
/**
* 評価値を小数点以下2桁にフォーマット
* @param {number|null} value - 評価値
* @param {string} fallback - 値がnullの場合の代替文字列(デフォルト: null)
* @returns {string|null} フォーマットされた評価値
*/
export const formatRating = (value, fallback = null) => {
return value != null ? Number(value).toFixed(2) : fallback;
};
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import StarRating from "@/Components/Lab/Star/StarRating";
import { formatRating } from "@/utils/formatRating";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem, overallAverage }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 総合評価と研究室概要 */}
<div>
{/* 総合評価 */}
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価
</h2>
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex items-center gap-2">
<StarRating rating={overallAverage || 0} />
<span className="text-sm text-[#F4BB42]">
{formatRating(overallAverage, "0.00")}
</span>
</div>
) : (
<p className="text-sm text-gray-500">まだ評価がありません</p>
)}
</div>
{/* 研究室概要 */}
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
import { router } from "@inertiajs/react";
import RankingBadge from '../../Components/Lab/RankingBadge';
import StarRating from "./Star/StarRating";
import { formatRating } from "@/utils/formatRating";
/**
* ソート条件に対応する評価値を取得
* @param {Object} lab - 研究室オブジェクト
* @param {string} sort - ソート条件のキー
* @returns {number|null} 評価値
*/
const getRatingValue = (lab, sort) => {
if (sort === 'reviews_count') {
return null; // レビュー数の場合は星評価を表示しない
}
const ratingMap = {
overall: lab.overall_avg,
mentorship_style: lab.avg_mentorship_style,
lab_atmosphere: lab.avg_lab_atmosphere,
achievement_activity: lab.avg_achievement_activity,
constraint_level: lab.avg_constraint_level,
facility_quality: lab.avg_facility_quality,
work_style: lab.avg_work_style,
student_balance: lab.avg_student_balance,
};
return ratingMap[sort] ?? lab.overall_avg;
};
/**
* 研究室カードコンポーネント
* @param {Object} props - コンポーネントのprops
* @param {Object} props.lab - 研究室オブジェクト
* @param {string} props.query - 検索クエリ文字列
* @param {string} props.sort - ソート条件
* @returns {JSX.Element} コンポーネントのJSX
*/
const LabCard = ({ lab, query, sort = 'overall' }) => {
// ソート条件に応じた評価値を取得
const ratingValue = getRatingValue(lab, sort);
const formattedReview = formatRating(ratingValue);
// レビュー数を取得(reviews_countまたはreviewsの配列長)
const reviewCount = lab.reviews_count ?? lab.reviews?.length ?? 0;
return (
<div
className="bg-[#EEF7FB] rounded-lg shadow-md px-4 py-3 hover:shadow-lg transition-shadow cursor-pointer flex items-start gap-4 relative"
onClick={() => router.get(route("labs.show", { lab: lab.id, query }))}
>
<div className="absolute top-1 left-1">
<RankingBadge rank={lab.rank} className="flex-shrink-0 text-3xl" />
</div>
<div className="ml-6">
<span className="text-2xl font-bold text-[#747D8C]">
{lab.name}
</span>
{formattedReview && (
<div className="flex items-center gap-2 mt-1">
<StarRating rating={ratingValue} />
<span className="text-sm text-[#F4BB42]">
{formattedReview}
</span>
</div>
)}
</div>
{/* 右下にレビュー数を表示 */}
<div className="absolute bottom-2 right-3">
<span className="text-sm text-[#747D8C]">
{reviewCount}件のレビュー
</span>
</div>
</div>
);
}
export default LabCard;
また、レビューの投稿数も表示しておきましょう。
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価({lab.reviews?.length || 0})
</h2>
研究室URLを追加
研究室のホームページを追加しましょう。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import StarRating from "@/Components/Lab/Star/StarRating";
import { formatRating } from "@/utils/formatRating";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem, overallAverage }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 総合評価と研究室概要 */}
<div>
{/* 総合評価 */}
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価({lab.reviews?.length || 0})
</h2>
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex items-center gap-2">
<StarRating rating={overallAverage || 0} />
<span className="text-sm text-[#F4BB42]">
{formatRating(overallAverage, "0.00")}
</span>
</div>
) : (
<p className="text-sm text-gray-500">まだ評価がありません</p>
)}
</div>
{/* 研究室概要 */}
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
{/* 研究室ページ */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室ページ
</h2>
{lab.url ? (
<a
href={lab.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline break-all"
>
{lab.url}
</a>
) : (
<p className="text-sm text-gray-500">URLはまだ登録されていません</p>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
教授情報を追加
教授の名前を追加します。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import StarRating from "@/Components/Lab/Star/StarRating";
import { formatRating } from "@/utils/formatRating";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem, overallAverage }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 総合評価と研究室概要 */}
<div>
{/* 総合評価 */}
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価({lab.reviews?.length || 0})
</h2>
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex items-center gap-2">
<StarRating rating={overallAverage || 0} />
<span className="text-sm text-[#F4BB42]">
{formatRating(overallAverage, "0.00")}
</span>
</div>
) : (
<p className="text-sm text-gray-500">まだ評価がありません</p>
)}
</div>
{/* 研究室概要 */}
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
{/* 研究室ページ */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室ページ
</h2>
{lab.url ? (
<a
href={lab.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline break-all"
>
{lab.url}
</a>
) : (
<p className="text-sm text-gray-500">URLはまだ登録されていません</p>
)}
</div>
{/* 教授 */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
教授
</h2>
<p className="text-sm text-gray-600">
{lab.professor_name ? `${lab.professor_name} 先生` : "教授名はまだ登録されていません"}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
男女比を追加する
男女比はバーで割合を表示させます。
import { Head } from "@inertiajs/react";
import AppLayout from "@/Layouts/AppLayout";
import StarRating from "@/Components/Lab/Star/StarRating";
import { formatRating } from "@/utils/formatRating";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
import { Radar } from "react-chartjs-2";
// Chart.jsのコンポーネントを登録
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
const Show = ({ lab, averagePerItem, overallAverage }) => {
// 7つの評価指標のラベル
const labels = [
"指導スタイル",
"雰囲気・文化",
"成果・活動",
"拘束度",
"設備",
"働き方",
"人数バランス",
];
// LabControllerから渡された平均評価データを配列に変換
const averageRatings = averagePerItem
? [
averagePerItem.mentorship_style || 0,
averagePerItem.lab_atmosphere || 0,
averagePerItem.achievement_activity || 0,
averagePerItem.constraint_level || 0,
averagePerItem.facility_quality || 0,
averagePerItem.work_style || 0,
averagePerItem.student_balance || 0,
]
: [0, 0, 0, 0, 0, 0, 0];
// レーダーチャートのデータ
const chartData = {
labels: labels,
datasets: [
{
label: "全投稿者の平均評価",
data: averageRatings,
backgroundColor: "rgba(51, 225, 237, 0.2)",
borderColor: "rgba(51, 225, 237, 1)",
borderWidth: 2,
pointBackgroundColor: "rgba(51, 225, 237, 1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(51, 225, 237, 1)",
},
],
};
// レーダーチャートのオプション
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
scales: {
r: {
angleLines: {
display: true,
},
suggestedMin: 0,
suggestedMax: 5,
ticks: {
stepSize: 1,
font: {
size: 12,
},
},
pointLabels: {
font: {
size: 14,
},
},
},
},
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${context.raw.toFixed(2)}`;
},
},
},
},
};
return (
<AppLayout title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`}>
<Head title={`${lab.faculty.university.name} ${lab.faculty.name} ${lab.name}`} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden sm:rounded-lg">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左側: レーダーチャート */}
<div className="flex justify-center items-start">
{lab.reviews && lab.reviews.length > 0 ? (
<div className="w-full max-w-sm">
<Radar data={chartData} options={chartOptions} />
</div>
) : (
<p className="text-center text-gray-500">
まだレビューがありません
</p>
)}
</div>
{/* 右側: 総合評価と研究室概要 */}
<div>
{/* 総合評価 */}
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
総合評価({lab.reviews?.length || 0})
</h2>
{lab.reviews && lab.reviews.length > 0 ? (
<div className="flex items-center gap-2">
<StarRating rating={overallAverage || 0} />
<span className="text-sm text-[#F4BB42]">
{formatRating(overallAverage, "0.00")}
</span>
</div>
) : (
<p className="text-sm text-gray-500">まだ評価がありません</p>
)}
</div>
{/* 研究室概要 */}
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室概要
</h2>
<p className="text-sm text-gray-600 whitespace-pre-wrap leading-tight">
{lab.description || "概要はまだ登録されていません"}
</p>
{/* 研究室ページ */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
研究室ページ
</h2>
{lab.url ? (
<a
href={lab.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline break-all"
>
{lab.url}
</a>
) : (
<p className="text-sm text-gray-500">URLはまだ登録されていません</p>
)}
</div>
{/* 教授 */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
教授
</h2>
<p className="text-sm text-gray-600">
{lab.professor_name ? `${lab.professor_name} 先生` : "教授名はまだ登録されていません"}
</p>
</div>
{/* 男女比 */}
<div className="mt-4">
<h2 className="text-base font-semibold text-gray-800 mb-2">
男女比{(lab.gender_ratio_male != null && lab.gender_ratio_female != null) && `(${lab.gender_ratio_male}:${lab.gender_ratio_female})`}
</h2>
{(lab.gender_ratio_male != null && lab.gender_ratio_female != null) ? (
<div className="flex w-full h-6 rounded overflow-hidden text-sm text-white font-medium">
{lab.gender_ratio_male > 0 && (
<div
className="flex items-center justify-center"
style={{
backgroundColor: "#7BB3CE",
width: `${(lab.gender_ratio_male / (lab.gender_ratio_male + lab.gender_ratio_female)) * 100}%`,
}}
>
男
</div>
)}
{lab.gender_ratio_female > 0 && (
<div
className="flex items-center justify-center"
style={{
backgroundColor: "#E89EB9",
width: `${(lab.gender_ratio_female / (lab.gender_ratio_male + lab.gender_ratio_female)) * 100}%`,
}}
>
女
</div>
)}
</div>
) : (
<p className="text-sm text-gray-500">男女比はまだ登録されていません</p>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
};
export default Show;
今回はここまでとします!
今回は記事を書く前から長くなるなと思っていました。
続きは、後編でお会いしましょう!
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
- その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: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"





