🙆
講座内課題ECサイト
- 概要
SIBAKAWA BOOK STOREという既存のECサイトに追加機能を実装していく課題を頑張りました。
最初は個人課題でお問い合わせフォームの作成を行いました。
最後は難しい課題をチーム開発で実施しましたので後述していきます。
エンジニア育成校のeveriGoではアクティブラーニングを採用しています。
課題が配られ自分で調べながらこなし、わからないところがあれば的確な質問をして、講師の方々から答えを引き出すといったスタンスの学校です。
- 使用技術
Laravel / JavaScript / MySQL / HTML / CSS
- Githubリンク
- 画面紹介
TOP画面
新刊情報 book/new-release
書籍情報 /book
書籍詳細 /book/id
書籍評価 /book/id/review
お知らせ /news
実装した追加機能・工夫したところ
星評価機能
工夫したところ
<form class="form" method="post">
@csrf
<p class="form-title">星評価</p>
<div class="form-rating">
@for ($i = 5; $i >= 1; $i--)
<input class="form-rating__input"
id="star{{ $i }}"
name="rating"
type="radio"
value="{{ $i }}"
{{ (old('rating', $book->reviews()->where('user_id', auth()->id())->first()->rating ?? 0) == $i) ? 'checked' : '' }}>
<label class="form-rating__label" for="star{{ $i }}">
<span class="star">★</span>
</label>
@endfor
</div>
@error('rating')
<span class="error">{{ $message }}</span><br>
@enderror
@error('title')
<span class="error">{{ $message }}</span><br>
@enderror
@error('content')
<span class="error">{{ $message }}</span><br>
@enderror
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" name="title" id="title" value="{{ old('title', $book->reviews()->where('user_id', auth()->id())->first()->title ?? '') }}" required>
</div>
<div class="form-group">
<label for="content">レビュー内容</label>
<textarea name="content" id="content" required>{{ old('content', $book->reviews()->where('user_id', auth()->id())->first()->content ?? '') }}</textarea>
</div>
<ul>
<li><button formaction="{{ route('book.sent', ['id' => $book->id]) }}" type="submit" class="review-post-button">投稿する</button></li>
<li><button formaction="{{ route('book.deleted', ['id' => $book->id]) }}" class="review-deleted-button">削除する</button></li>
</ul>
</form>
old関数で以前の入力内容や保存済みレビューを反映しました。
<button formaction="..."> を使い、同じフォームから「投稿する」と「削除する」を切り替えられるようにしました。
ページネーション
星評価のグラフ
工夫したところ
document.addEventListener('DOMContentLoaded', function () {
// #ratingChartが存在するか確認
const ratingChartElement = document.getElementById('ratingChart')
// #ratingChartが見つからない場合、エラーメッセージを表示して処理を終了
if (!ratingChartElement) {
return // 処理をここで停止
}
// ここから先は#ratingChartが存在する場合のみ実行されます
const currentUrl = ratingChartElement.dataset.currentUrl
// データ取得
const labels = ['星5つ', '星4つ', '星3つ', '星2つ', '星1つ']
const data = JSON.parse(ratingChartElement.dataset.starCounts)
const totalReviews = data.reduce((sum, value) => sum + value, 0)
const percentages = data.map((value) =>
Math.round((value / totalReviews) * 100)
)
// カスタムプラグインでパーセンテージ表示
const barPercentagePlugin = {
id: 'barPercentagePlugin',
afterDatasetsDraw(chart) {
const {
ctx,
chartArea,
scales: { x, y },
} = chart
const chartWidth = chartArea.right
chart.data.datasets[0].data.forEach((value, index) => {
const percentageText = `${percentages[index]}%`
let xPosition = x.getPixelForValue(value)
const yPosition = y.getPixelForValue(index)
const textWidth = ctx.measureText(percentageText).width
if (xPosition + textWidth + 10 > chartWidth) {
xPosition = chartWidth - textWidth - 10
}
ctx.save()
ctx.font = '14px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'left'
ctx.textBaseline = 'middle'
ctx.fillText(percentageText, xPosition + 5, yPosition)
ctx.restore()
})
},
}
const maxValue = Math.max(...data)
// グラフ設定
const config = {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '評価の数',
data: data,
backgroundColor: [
'rgba(255, 204, 0, 0.5)',
'rgba(255, 204, 0, 0.5)',
'rgba(255, 204, 0, 0.5)',
'rgba(255, 204, 0, 0.5)',
'rgba(255, 204, 0, 0.5)',
],
borderColor: '',
borderWidth: 0,
hoverBackgroundColor: 'rgba(255, 204, 0, 1)',
},
],
},
options: {
indexAxis: 'y',
scales: {
x: {
beginAtZero: true,
grid: {
display: false, // x軸のグリッド線を非表示
},
ticks: {
display: false, // x軸のラベルを非表示
},
max: maxValue * 1.15,
},
y: {
grid: {
display: false, // y軸のグリッド線を非表示
},
ticks: {
font: {
family: "'Yu Gothic Medium', '游ゴシック Medium', YuGothic, '游ゴシック体', 'ヒラギノ角ゴ Pro W3', 'メイリオ', sans-serif",
size: 14,
},
color: 'black',
}
},
},
plugins: {
tooltip: {
enabled: false, // ツールチップを非表示
},
title: {
display: true,
text: `${totalReviews}件の評価`,
font: {
family: "'Yu Gothic Medium', '游ゴシック Medium', YuGothic, '游ゴシック体', 'ヒラギノ角ゴ Pro W3', 'メイリオ', sans-serif", // フォントファミリー
size: 16,
weight: 'normal',
},
color: 'black',
padding: {
top: 10,
bottom: 10,
},
},
legend: {
display: false,
},
},
onClick: (event, chartElement) => {
if (chartElement.length) {
const dataIndex = chartElement[0].index
const star = 5 - dataIndex
// 現在のURLオブジェクトを作成
const url = new URL(window.location.href)
// `star` パラメータを更新
url.searchParams.set('star', star)
// 既存の `order_rule` を維持
const orderRule = url.searchParams.get('order_rule')
if (orderRule) {
url.searchParams.set('order_rule', orderRule)
}
// ハッシュを `review-section` に設定
url.hash = 'review-section'
// 新しいURLに遷移
window.location.href = url.toString()
}
},
onHover: (event, chartElement) => {
const canvas = event.native.target
canvas.style.cursor = chartElement.length
? 'pointer'
: 'default'
},
},
plugins: [barPercentagePlugin],
}
// グラフのレンダリング
const ctx = ratingChartElement.getContext('2d')
new Chart(ctx, config)
})
chart.jsを使いグラフを実装しました。
good、bad機能
工夫したところ
document.addEventListener('DOMContentLoaded', function () {
// 初期状態を設定
document.querySelectorAll('.reaction-button').forEach((button) => {
const reviewId = button.dataset.reviewId
fetch(`/review/${reviewId}/reaction-status`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((data) => {
if (data.reaction !== null) {
const goodButton = document.querySelector(
`button[data-review-id="${reviewId}"].good`
)
const badButton = document.querySelector(
`button[data-review-id="${reviewId}"].bad`
)
if (data.reaction === 0) {
goodButton.classList.add('active')
badButton.classList.remove('active')
} else if (data.reaction === 1) {
badButton.classList.add('active')
goodButton.classList.remove('active')
}
}
})
.catch((error) => {
console.error('Error:', error)
})
})
// ボタンのクリックイベントを設定
document.querySelectorAll('.reaction-button').forEach((button) => {
button.addEventListener('click', function () {
const reviewId = this.dataset.reviewId
const reaction = parseInt(this.dataset.reaction, 10)
const bookId = this.closest('.review-item')?.dataset.bookId
if (!bookId) {
console.error('Error: bookId not found.')
return
}
const goodButton = document.querySelector(
`button[data-review-id="${reviewId}"].good`
)
const badButton = document.querySelector(
`button[data-review-id="${reviewId}"].bad`
)
let newReaction = reaction
// すでに good/bad が押されている場合、リアクションを取り消し
if (
(reaction === 0 && goodButton.classList.contains('active')) ||
(reaction === 1 && badButton.classList.contains('active'))
) {
newReaction = null // null を送信してリアクションを削除
}
fetch(`/book/${bookId}/review/reaction`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector(
'meta[name="csrf-token"]'
).content,
},
body: JSON.stringify({
review_id: reviewId,
reaction: newReaction,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
console.error('Server Error:', data.error)
return
}
document.querySelector(
`button[data-review-id="${reviewId}"].good .good-count`
).textContent = data.good_count
document.querySelector(
`button[data-review-id="${reviewId}"].bad .bad-count`
).textContent = data.bad_count
// リアクションの状態を更新
if (data.reaction_state === null) {
// リアクションが取り消された場合
goodButton.classList.remove('active')
badButton.classList.remove('active')
} else if (data.reaction_state === 0) {
// good が押された場合
goodButton.classList.add('active')
badButton.classList.remove('active')
} else {
// bad が押された場合
badButton.classList.add('active')
goodButton.classList.remove('active')
}
})
.catch((error) => {
console.error('Error:', error)
})
})
})
})
Ajax(非同期通信)を使いgood、bad機能を実装しました。
レビューの表示順の並び替えと表示件数の変更
工夫したところ
view
<select id="order_rule">
@php
$order_rules = [
'updated_date_desc' => '新しい順',
'updated_date_asc' => '古い順',
'reactions_desc' => 'リアクションの多い順',
'reactions_asc' => 'リアクションの少ない順',
];
@endphp
@foreach ($order_rules as $key => $name)
<option value="{{ $key }}" {{ $order_rule == $key ? 'selected' : '' }}>
{{ $name }}
</option>
@endforeach
</select>
BookRepository
// 並び替え処理
switch ($order_rule) {
case 'updated_date_asc':
$reviews->orderBy('reviews.updated_at', 'asc');
break;
case 'updated_date_desc':
$reviews->orderBy('reviews.updated_at', 'desc');
break;
case 'reactions_asc':
$reviews->orderBy('reaction_count', 'asc');
break;
case 'reactions_desc':
$reviews->orderBy('reaction_count', 'desc');
break;
default:
$reviews->orderBy('reviews.updated_at', 'desc');
break;
}
viewのプルダウンメニューに対応した並び替え処理をbookリポジトリにて実装しました。
動画ではプルダウンメニューが写っていませんがしっかりと表示されています。
Discussion