いいねボタンの追加

This commit is contained in:
shibafu 2019-04-05 23:10:26 +09:00
parent 34b7cd6c89
commit 225d0854ef
16 changed files with 158 additions and 120 deletions

View File

@ -2,7 +2,9 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class Ejaculation extends Model class Ejaculation extends Model
{ {
@ -34,4 +36,25 @@ class Ejaculation extends Model
return $v->name; return $v->name;
})->all()); })->all());
} }
public function likes()
{
return $this->hasMany(Like::class);
}
public function scopeWithLikes(Builder $query)
{
if (Auth::check()) {
// (ejaculation_id, user_id) でユニークなわけですが、サブクエリ発行させるのとLeft JoinしてNULLかどうかで結果を見るのどっちがいいんでしょうね
return $query->withCount([
'likes',
'likes as is_liked' => function ($query) {
$query->where('user_id', Auth::id());
}
]);
} else {
return $query->withCount('likes')
->addSelect('0 as is_liked');
}
}
} }

View File

@ -78,7 +78,9 @@ class EjaculationController extends Controller
public function show($id) public function show($id)
{ {
$ejaculation = Ejaculation::findOrFail($id); $ejaculation = Ejaculation::where('id', $id)
->withLikes()
->firstOrFail();
$user = User::findOrFail($ejaculation->user_id); $user = User::findOrFail($ejaculation->user_id);
// 1つ前のチェックインからの経過時間を求める // 1つ前のチェックインからの経過時間を求める

View File

@ -69,6 +69,7 @@ SQL
->orderBy('ejaculations.ejaculated_date', 'desc') ->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*') ->select('ejaculations.*')
->with('user', 'tags') ->with('user', 'tags')
->withLikes()
->take(10) ->take(10)
->get(); ->get();

View File

@ -21,6 +21,7 @@ class SearchController extends Controller
->where('is_private', false) ->where('is_private', false)
->orderBy('ejaculated_date', 'desc') ->orderBy('ejaculated_date', 'desc')
->with(['user', 'tags']) ->with(['user', 'tags'])
->withLikes()
->paginate(20) ->paginate(20)
->appends($inputs); ->appends($inputs);

View File

@ -16,6 +16,7 @@ class TimelineController extends Controller
->orderBy('ejaculations.ejaculated_date', 'desc') ->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*') ->select('ejaculations.*')
->with('user', 'tags') ->with('user', 'tags')
->withLikes()
->paginate(21); ->paginate(21);
return view('timeline.public')->with(compact('ejaculations')); return view('timeline.public')->with(compact('ejaculations'));

View File

@ -41,6 +41,7 @@ SQL
} }
$ejaculations = $query->orderBy('ejaculated_date', 'desc') $ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags') ->with('tags')
->withLikes()
->paginate(20); ->paginate(20);
// よく使っているタグ // よく使っているタグ

View File

@ -51,4 +51,9 @@ class User extends Authenticatable
{ {
return Auth::check() && $this->id === Auth::user()->id; return Auth::check() && $this->id === Auth::user()->id;
} }
public function likes()
{
return $this->hasMany(Like::class);
}
} }

View File

@ -27,4 +27,69 @@ $(() => {
event.preventDefault(); event.preventDefault();
$deleteCheckinModal.modal('show', this); $deleteCheckinModal.modal('show', this);
}); });
$(document).on('click', '[data-href]', function (event) {
location.href = $(this).data('href');
});
$(document).on('click', '.like-button', function (event) {
event.preventDefault();
const $this = $(this);
const targetId = $this.data('id');
const isLiked = $this.data('liked');
if (isLiked) {
const callback = (data) => {
$this.data('liked', false);
$this.find('.oi-heart').removeClass('text-danger');
const count = data.ejaculation ? data.ejaculation.likes_count : 0;
$this.find('.like-count').text(count ? count : '');
};
$.ajax({
url: '/api/likes/' + encodeURIComponent(targetId),
method: 'delete',
type: 'json'
})
.then(callback)
.catch(function (xhr) {
if (xhr.status === 404) {
callback(JSON.parse(xhr.responseText));
return;
}
console.error(xhr);
alert('いいねを解除できませんでした。');
});
} else {
const callback = (data) => {
$this.data('liked', true);
$this.find('.oi-heart').addClass('text-danger');
const count = data.ejaculation ? data.ejaculation.likes_count : 0;
$this.find('.like-count').text(count ? count : '');
};
$.ajax({
url: '/api/likes',
method: 'post',
type: 'json',
data: {
id: targetId
}
})
.then(callback)
.catch(function (xhr) {
if (xhr.status === 409) {
callback(JSON.parse(xhr.responseText));
return;
}
console.error(xhr);
alert('いいねできませんでした。');
});
}
});
}); });

View File

@ -13,3 +13,11 @@ $primary: #e53fb1;
// Components // Components
@import "components/link-card"; @import "components/link-card";
.like-button {
text-decoration: none !important;
}
.like-count:not(:empty) {
padding-left: 0.25rem;
}

View File

@ -40,10 +40,6 @@
border-top: none; border-top: none;
} }
.timeline-action-item {
margin-left: 16px;
}
.tis-global-count-graph { .tis-global-count-graph {
height: 90px; height: 90px;
border-bottom: 1px solid rgba(0, 0, 0, .125); border-bottom: 1px solid rgba(0, 0, 0, .125);

View File

@ -0,0 +1,35 @@
<!-- span -->
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> <bdi>{{ $ejaculation->user->display_name }}</bdi></a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
<div>
<button type="button" class="btn btn-link text-secondary like-button" data-toggle="tooltip" data-placement="bottom" data-trigger="hover" title="いいね" data-id="{{ $ejaculation->id }}" data-liked="{{ (bool)$ejaculation->is_liked }}"><span class="oi oi-heart {{ $ejaculation->is_liked ? 'text-danger' : '' }}"></span><span class="like-count">{{ $ejaculation->likes_count ? $ejaculation->likes_count : '' }}</span></button>
<button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン" data-href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload"></span></button>
</div>
</div>
<!-- tags -->
@if ($ejaculation->tags->isNotEmpty())
<p class="mb-2">
@foreach ($ejaculation->tags as $tag)
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="row mx-0">
@component('components.link-card', ['link' => $ejaculation->link])
@endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif

View File

@ -33,10 +33,11 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5> <h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
<div> <div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a> <button type="button" class="btn btn-link text-secondary like-button" data-toggle="tooltip" data-placement="bottom" data-trigger="hover" title="いいね" data-id="{{ $ejaculation->id }}" data-liked="{{ (bool)$ejaculation->is_liked }}"><span class="oi oi-heart {{ $ejaculation->is_liked ? 'text-danger' : '' }}"></span><span class="like-count">{{ $ejaculation->likes_count ? $ejaculation->likes_count : '' }}</span></button>
<button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン" data-href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload"></span></button>
@if ($user->isMe()) @if ($user->isMe())
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a> <button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="修正" data-href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil"></span></button>
<a class="text-secondary timeline-action-item" href="#" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a> <button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="削除" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash"></span></button>
@endif @endif
</div> </div>
</div> </div>

View File

@ -55,40 +55,8 @@
<ul class="list-group"> <ul class="list-group">
@foreach ($publicLinkedEjaculations as $ejaculation) @foreach ($publicLinkedEjaculations as $ejaculation)
<li class="list-group-item no-side-border pt-3 pb-3 text-break"> <li class="list-group-item no-side-border pt-3 pb-3 text-break">
<!-- span --> @component('components.ejaculation', compact('ejaculation'))
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> <bdi>{{ $ejaculation->user->display_name }}</bdi></a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
</div>
</div>
<!-- tags -->
@if ($ejaculation->tags->isNotEmpty())
<p class="mb-2">
@foreach ($ejaculation->tags as $tag)
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="row mx-0">
@component('components.link-card', ['link' => $ejaculation->link])
@endcomponent @endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
</li> </li>
@endforeach @endforeach
<li class="list-group-item no-side-border text-right"> <li class="list-group-item no-side-border text-right">

View File

@ -7,43 +7,8 @@
<ul class="list-group"> <ul class="list-group">
@foreach($results as $ejaculation) @foreach($results as $ejaculation)
<li class="list-group-item border-bottom-only pt-3 pb-3 text-break"> <li class="list-group-item border-bottom-only pt-3 pb-3 text-break">
<!-- span --> @component('components.ejaculation', compact('ejaculation'))
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> <bdi>{{ $ejaculation->user->display_name }}</bdi></a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
</div>
</div>
<!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
<p class="mb-2">
@if ($ejaculation->is_private)
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
@endif
@foreach ($ejaculation->tags as $tag)
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="row mx-0">
@component('components.link-card', ['link' => $ejaculation->link])
@endcomponent @endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
</li> </li>
@endforeach @endforeach
</ul> </ul>

View File

@ -11,43 +11,8 @@
<div class="row mx-1"> <div class="row mx-1">
@foreach($ejaculations as $ejaculation) @foreach($ejaculations as $ejaculation)
<div class="col-12 col-lg-6 col-xl-4 py-3 text-break border-top"> <div class="col-12 col-lg-6 col-xl-4 py-3 text-break border-top">
<!-- span --> @component('components.ejaculation', compact('ejaculation'))
<div class="d-flex justify-content-between">
<h5>
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> <bdi>{{ $ejaculation->user->display_name }}</bdi></a>
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
</h5>
<div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
</div>
</div>
<!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
<p class="mb-2">
@if ($ejaculation->is_private)
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
@endif
@foreach ($ejaculation->tags as $tag)
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
@endforeach
</p>
@endif
<!-- okazu link -->
@if (!empty($ejaculation->link))
<div class="row mx-0">
@component('components.link-card', ['link' => $ejaculation->link])
@endcomponent @endcomponent
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@ -39,10 +39,11 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5> <h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5>
<div> <div>
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a> <button type="button" class="btn btn-link text-secondary like-button" data-toggle="tooltip" data-placement="bottom" data-trigger="hover" title="いいね" data-id="{{ $ejaculation->id }}" data-liked="{{ (bool)$ejaculation->is_liked }}"><span class="oi oi-heart {{ $ejaculation->is_liked ? 'text-danger' : '' }}"></span><span class="like-count">{{ $ejaculation->likes_count ? $ejaculation->likes_count : '' }}</span></button>
<button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン" data-href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload"></span></button>
@if ($user->isMe()) @if ($user->isMe())
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a> <button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="修正" data-href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil"></span></button>
<a class="text-secondary timeline-action-item" href="#" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a> <button type="button" class="btn btn-link text-secondary" data-toggle="tooltip" data-placement="bottom" title="削除" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash"></span></button>
@endif @endif
</div> </div>
</div> </div>