Merge pull request #173 from shikorism/koresuki

チェックインに対する「いいね」機能
This commit is contained in:
shibafu 2019-04-17 19:55:42 +09:00 committed by GitHub
commit 676793cfe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 535 additions and 136 deletions

View File

@ -2,11 +2,15 @@
namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
class Ejaculation extends Model
{
//
use HasEagerLimit;
protected $fillable = [
'user_id', 'ejaculated_date',
@ -34,4 +38,45 @@ class Ejaculation extends Model
return $v->name;
})->all());
}
public function likes()
{
return $this->hasMany(Like::class);
}
public function scopeWithLikes(Builder $query)
{
if (Auth::check()) {
// TODO - このスコープを使うことでlikesが常に直近10件で絞られるのは汚染されすぎ感がある。別名を付与できないか
// - (ejaculation_id, user_id) でユニークなわけですが、is_liked はサブクエリ発行させるのとLeft JoinしてNULLかどうかで結果を見るのどっちがいいんでしょうね
return $query
->with([
'likes' => function ($query) {
$query->latest()->take(10);
},
'likes.user' => function ($query) {
$query->where('is_protected', false)
->orWhere('id', Auth::id());
}
])
->withCount([
'likes',
'likes as is_liked' => function ($query) {
$query->where('user_id', Auth::id());
}
]);
} else {
return $query
->with([
'likes' => function ($query) {
$query->latest()->take(10);
},
'likes.user' => function ($query) {
$query->where('is_protected', false);
}
])
->withCount('likes')
->addSelect(DB::raw('0 as is_liked'));
}
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api;
use App\Ejaculation;
use App\Http\Controllers\Controller;
use App\Like;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class LikeController extends Controller
{
public function store(Request $request)
{
$request->validate([
'id' => 'required|integer|exists:ejaculations'
]);
$keys = [
'user_id' => Auth::id(),
'ejaculation_id' => $request->input('id')
];
$like = Like::query()->where($keys)->first();
if ($like) {
$data = [
'errors' => [
['message' => 'このチェックインはすでにいいね済です。']
],
'ejaculation' => $like->ejaculation
];
return response()->json($data, 409);
}
$like = Like::create($keys);
return [
'ejaculation' => $like->ejaculation
];
}
public function destroy($id)
{
Validator::make(compact('id'), [
'id' => 'required|integer'
])->validate();
$like = Like::query()->where([
'user_id' => Auth::id(),
'ejaculation_id' => $id
])->first();
if ($like === null) {
$ejaculation = Ejaculation::find($id);
$data = [
'errors' => [
['message' => 'このチェックインはいいねされていません。']
],
'ejaculation' => $ejaculation
];
return response()->json($data, 404);
}
$like->delete();
return [
'ejaculation' => $like->ejaculation
];
}
}

View File

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

View File

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

View File

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

View File

@ -46,11 +46,12 @@ class SettingController extends Controller
public function updatePrivacy(Request $request)
{
$inputs = $request->all(['is_protected', 'accept_analytics']);
$inputs = $request->all(['is_protected', 'accept_analytics', 'private_likes']);
$user = Auth::user();
$user->is_protected = $inputs['is_protected'] ?? false;
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
$user->private_likes = $inputs['private_likes'] ?? false;
$user->save();
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');

View File

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

View File

@ -41,6 +41,7 @@ SQL
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
->withLikes()
->paginate(20);
// よく使っているタグ
@ -176,4 +177,19 @@ SQL
return view('user.profile')->with(compact('user', 'ejaculations'));
}
public function likes($name)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
$likes = $user->likes()
->orderBy('created_at', 'desc')
->with('ejaculation.user', 'ejaculation.tags')
->paginate(20);
return view('user.likes')->with(compact('user', 'likes'));
}
}

View File

@ -38,7 +38,12 @@ class Kernel extends HttpKernel
\App\Http\Middleware\NormalizeLineEnding::class,
],
// 現時点では内部APIしかないので、認証の手間を省くためにステートフルにしている。
'api' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
'throttle:60,1',
'bindings',
],

23
app/Like.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
class Like extends Model
{
use HasEagerLimit;
protected $fillable = ['user_id', 'ejaculation_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function ejaculation()
{
return $this->belongsTo(Ejaculation::class)->withLikes();
}
}

View File

@ -20,6 +20,7 @@ class User extends Authenticatable
'is_protected', 'accept_analytics',
'display_name', 'description',
'twitter_id', 'twitter_name',
'private_likes',
];
/**
@ -51,4 +52,9 @@ class User extends Authenticatable
{
return Auth::check() && $this->id === Auth::user()->id;
}
public function likes()
{
return $this->hasMany(Like::class);
}
}

View File

@ -12,7 +12,8 @@
"guzzlehttp/guzzle": "^6.3",
"laravel/framework": "5.5.*",
"laravel/tinker": "~1.0",
"misd/linkify": "^1.1"
"misd/linkify": "^1.1",
"staudenmeir/eloquent-eager-limit": "^1.0"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1",

44
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "79423bebbfa31e28aab2d06ba7e19828",
"content-hash": "665f6f5eb180a1295fb60303d2ea5051",
"packages": [
{
"name": "anhskohbo/no-captcha",
@ -1873,6 +1873,48 @@
],
"time": "2018-07-19T23:38:55+00:00"
},
{
"name": "staudenmeir/eloquent-eager-limit",
"version": "v1.2",
"source": {
"type": "git",
"url": "https://github.com/staudenmeir/eloquent-eager-limit.git",
"reference": "4ee5c70268b5bc019618c7de66145d7addb3c5dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/staudenmeir/eloquent-eager-limit/zipball/4ee5c70268b5bc019618c7de66145d7addb3c5dc",
"reference": "4ee5c70268b5bc019618c7de66145d7addb3c5dc",
"shasum": ""
},
"require": {
"illuminate/database": "~5.5.43|~5.6.34|5.7.*",
"illuminate/support": "^5.5.14",
"php": ">=7.0"
},
"require-dev": {
"laravel/homestead": "^7.18",
"phpunit/phpunit": "~6.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Staudenmeir\\EloquentEagerLimit\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonas Staudenmeir",
"email": "mail@jonas-staudenmeir.de"
}
],
"description": "Laravel Eloquent eager loading with limit",
"time": "2019-01-23T19:25:27+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.1.3",

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLikesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('likes', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->index();
$table->integer('ejaculation_id')->index();
$table->timestamps();
$table->unique(['user_id', 'ejaculation_id']);
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('ejaculation_id')->references('id')->on('ejaculations')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('likes');
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPrivateLikesToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('private_likes')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('private_likes');
});
}
}

View File

@ -27,4 +27,69 @@ $(() => {
event.preventDefault();
$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

@ -12,4 +12,5 @@ $primary: #e53fb1;
@import "tissue.css";
// Components
@import "components/ejaculation";
@import "components/link-card";

View File

@ -0,0 +1,21 @@
.ejaculation-actions {
& > button:not(:last-child) {
margin-right: 24px;
}
}
.like-button {
text-decoration: none !important;
}
.like-count:not(:empty) {
padding-left: 0.5rem;
}
.like-users {
height: 30px;
}
.like-users-tall {
height: 36px;
}

View File

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

View File

@ -0,0 +1,53 @@
<!-- span -->
<div>
<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>
<!-- 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-2 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
<!-- likes -->
@if ($ejaculation->likes_count > 0)
<div class="my-2 py-1 border-top border-bottom d-flex align-items-center">
<div class="ml-2 mr-3 text-secondary flex-shrink-0"><small><strong>{{ $ejaculation->likes_count }}</strong> 件のいいね</small></div>
<div class="like-users flex-grow-1 overflow-hidden">
@foreach ($ejaculation->likes as $like)
@if ($like->user !== null)
<a href="{{ route('user.profile', ['name' => $like->user->name]) }}"><img src="{{ $like->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded" data-toggle="tooltip" data-placement="bottom" title="{{ $like->user->display_name }}"></a>
@endif
@endforeach
</div>
</div>
@endif
<!-- actions -->
<div class="ejaculation-actions">
<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>
<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>
</div>

View File

@ -30,15 +30,8 @@
<div class="card">
<div class="card-body">
<!-- span -->
<div class="d-flex justify-content-between">
<div>
<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>
<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>
@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>
<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>
@endif
</div>
</div>
<!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
@ -63,10 +56,32 @@
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
<p class="mb-2 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
<!-- likes -->
@if ($ejaculation->likes_count > 0)
<div class="my-2 py-1 border-top border-bottom d-flex align-items-center">
<div class="ml-2 mr-3 text-secondary flex-shrink-0"><small><strong>{{ $ejaculation->likes_count }}</strong> 件のいいね</small></div>
<div class="like-users-tall flex-grow-1 overflow-hidden">
@foreach ($ejaculation->likes as $like)
@if ($like->user !== null)
<a href="{{ route('user.profile', ['name' => $like->user->name]) }}"><img src="{{ $like->user->getProfileImageUrl(36) }}" width="36" height="36" class="rounded" data-toggle="tooltip" data-placement="bottom" title="{{ $like->user->display_name }}"></a>
@endif
@endforeach
</div>
</div>
@endif
<!-- actions -->
<div class="ejaculation-actions">
<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>
<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>
@if ($user->isMe())
<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>
<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
</div>
</div>
</div>
@endif

View File

@ -55,40 +55,8 @@
<ul class="list-group">
@foreach ($publicLinkedEjaculations as $ejaculation)
<li class="list-group-item no-side-border pt-3 pb-3 text-break">
<!-- 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>
<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
<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
@component('components.ejaculation', compact('ejaculation'))
@endcomponent
</li>
@endforeach
<li class="list-group-item no-side-border text-right">

View File

@ -50,6 +50,9 @@
</p>
</a>
<div class="dropdown-divider"></div>
<a href="{{ route('user.profile', ['name' => Auth::user()->name]) }}" class="dropdown-item">プロフィール</a>
<a href="{{ route('user.likes', ['name' => Auth::user()->name]) }}" class="dropdown-item">いいね</a>
<div class="dropdown-divider"></div>
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
</div>
@ -104,6 +107,9 @@
</p>
</a>
<div class="dropdown-divider"></div>
<a href="{{ route('user.profile', ['name' => Auth::user()->name]) }}" class="dropdown-item">プロフィール</a>
<a href="{{ route('user.likes', ['name' => Auth::user()->name]) }}" class="dropdown-item">いいね</a>
<div class="dropdown-divider"></div>
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
@can ('admin')
<a href="{{ route('admin.dashboard') }}" class="dropdown-item">管理</a>

View File

@ -7,43 +7,8 @@
<ul class="list-group">
@foreach($results as $ejaculation)
<li class="list-group-item border-bottom-only pt-3 pb-3 text-break">
<!-- 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>
<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
<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
@component('components.ejaculation', compact('ejaculation'))
@endcomponent
</li>
@endforeach
</ul>

View File

@ -11,6 +11,11 @@
<div class="custom-control custom-checkbox mb-2">
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ (old('is_protected') ?? Auth::user()->is_protected ) ? 'checked' : '' }}>
<label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
<small class="form-text text-muted">プロフィール情報を除いて、全ての情報が非公開になります。</small>
</div>
<div class="custom-control custom-checkbox mb-2">
<input id="private-likes" name="private_likes" class="custom-control-input" type="checkbox" {{ (old('private_likes') ?? Auth::user()->private_likes ) ? 'checked' : '' }}>
<label class="custom-control-label" for="private-likes">いいねしたチェックイン一覧を非公開にする</label>
</div>
<div class="custom-control custom-checkbox">
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ (old('accept_analytics') ?? Auth::user()->accept_analytics ) ? 'checked' : '' }}>

View File

@ -11,43 +11,8 @@
<div class="row mx-1">
@foreach($ejaculations as $ejaculation)
<div class="col-12 col-lg-6 col-xl-4 py-3 text-break border-top">
<!-- 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>
<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
<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
@component('components.ejaculation', compact('ejaculation'))
@endcomponent
</div>
@endforeach
</div>

View File

@ -20,6 +20,13 @@
<li class="nav-item">
<a class="nav-link {{ Route::currentRouteName() === 'user.okazu' ? 'active' : '' }}" href="{{ route('user.okazu', ['name' => $user->name]) }}">オカズ</a>
</li>
<li class="nav-item">
<a class="nav-link {{ Route::currentRouteName() === 'user.likes' ? 'active' : '' }}" href="{{ route('user.likes', ['name' => $user->name]) }}">いいね
@if ($user->isMe() || !($user->is_protected || $user->private_likes))
<span class="badge badge-primary">{{ $user->likes()->count() }}</span>
@endif
</a>
</li>
</ul>
<div class="tab-content">
@yield('tab-content')

View File

@ -0,0 +1,25 @@
@extends('user.base')
@section('title', $user->display_name . ' (@' . $user->name . ') さんがいいねしたチェックイン')
@section('tab-content')
@if (($user->is_protected || $user->private_likes) && !$user->isMe())
<p class="mt-4">
<span class="oi oi-lock-locked"></span> このユーザはいいね一覧を公開していません。
</p>
@else
<ul class="list-group">
@forelse ($likes as $like)
<li class="list-group-item border-bottom-only pt-3 pb-3 text-break">
@component('components.ejaculation', ['ejaculation' => $like->ejaculation])
@endcomponent
</li>
@empty
<li class="list-group-item border-bottom-only">
<p>まだ何もいいと思ったことがありません。</p>
</li>
@endforelse
</ul>
{{ $likes->links(null, ['className' => 'mt-4 justify-content-center']) }}
@endif
@endsection

View File

@ -36,15 +36,8 @@
@forelse ($ejaculations as $ejaculation)
<li class="list-group-item border-bottom-only pt-3 pb-3 text-break">
<!-- span -->
<div class="d-flex justify-content-between">
<div>
<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>
<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>
@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>
<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>
@endif
</div>
</div>
<!-- tags -->
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
@ -69,10 +62,32 @@
@endif
<!-- note -->
@if (!empty($ejaculation->note))
<p class="mb-0 text-break">
<p class="mb-2 text-break">
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
</p>
@endif
<!-- likes -->
@if ($ejaculation->likes_count > 0)
<div class="my-2 py-1 border-top border-bottom d-flex align-items-center">
<div class="ml-2 mr-3 text-secondary flex-shrink-0"><small><strong>{{ $ejaculation->likes_count }}</strong> 件のいいね</small></div>
<div class="like-users flex-grow-1 overflow-hidden">
@foreach ($ejaculation->likes as $like)
@if ($like->user !== null)
<a href="{{ route('user.profile', ['name' => $like->user->name]) }}"><img src="{{ $like->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded" data-toggle="tooltip" data-placement="bottom" title="{{ $like->user->display_name }}"></a>
@endif
@endforeach
</div>
</div>
@endif
<!-- actions -->
<div class="ejaculation-actions">
<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>
<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>
@if ($user->isMe())
<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>
<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
</div>
</li>
@empty
<li class="list-group-item border-bottom-only">

View File

@ -16,3 +16,8 @@
//});
Route::get('/checkin/card', 'Api\\CardController@show');
Route::middleware('auth')->group(function () {
Route::post('/likes', 'Api\\LikeController@store');
Route::delete('/likes/{id}', 'Api\\LikeController@destroy');
});

View File

@ -19,6 +19,7 @@ Route::get('/user', 'UserController@redirectMypage')->middleware('auth');
Route::get('/user/{name?}', 'UserController@profile')->name('user.profile');
Route::get('/user/{name}/stats', 'UserController@stats')->name('user.stats');
Route::get('/user/{name}/okazu', 'UserController@okazu')->name('user.okazu');
Route::get('/user/{name}/likes', 'UserController@likes')->name('user.likes');
Route::get('/checkin/{id}', 'EjaculationController@show')->name('checkin.show');
Route::middleware('auth')->group(function () {