diff --git a/app/Ejaculation.php b/app/Ejaculation.php index 83b3ad2..24f7e46 100644 --- a/app/Ejaculation.php +++ b/app/Ejaculation.php @@ -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')); + } + } } diff --git a/app/Http/Controllers/Api/LikeController.php b/app/Http/Controllers/Api/LikeController.php new file mode 100644 index 0000000..d782956 --- /dev/null +++ b/app/Http/Controllers/Api/LikeController.php @@ -0,0 +1,73 @@ +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 + ]; + } +} diff --git a/app/Http/Controllers/EjaculationController.php b/app/Http/Controllers/EjaculationController.php index d2292e0..13ef8c4 100644 --- a/app/Http/Controllers/EjaculationController.php +++ b/app/Http/Controllers/EjaculationController.php @@ -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つ前のチェックインからの経過時間を求める diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 95807a5..fb8147f 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -69,6 +69,7 @@ SQL ->orderBy('ejaculations.ejaculated_date', 'desc') ->select('ejaculations.*') ->with('user', 'tags') + ->withLikes() ->take(10) ->get(); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 271a003..0487344 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -28,6 +28,7 @@ class SearchController extends Controller ->where('is_private', false) ->orderBy('ejaculated_date', 'desc') ->with(['user', 'tags']) + ->withLikes() ->paginate(20) ->appends($inputs); diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 136f055..1448360 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -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', 'プライバシー設定を更新しました。'); diff --git a/app/Http/Controllers/TimelineController.php b/app/Http/Controllers/TimelineController.php index a179537..2f05b71 100644 --- a/app/Http/Controllers/TimelineController.php +++ b/app/Http/Controllers/TimelineController.php @@ -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')); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1640c78..5bdc9b5 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -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')); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 95714cf..dfa1743 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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', ], diff --git a/app/Like.php b/app/Like.php new file mode 100644 index 0000000..b1fa673 --- /dev/null +++ b/app/Like.php @@ -0,0 +1,23 @@ +belongsTo(User::class); + } + + public function ejaculation() + { + return $this->belongsTo(Ejaculation::class)->withLikes(); + } +} diff --git a/app/User.php b/app/User.php index 5469e52..ec9c4d7 100644 --- a/app/User.php +++ b/app/User.php @@ -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); + } } diff --git a/composer.json b/composer.json index 27bdca3..c28387c 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 6766e6b..344bbc1 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/database/migrations/2019_03_26_224641_create_likes_table.php b/database/migrations/2019_03_26_224641_create_likes_table.php new file mode 100644 index 0000000..773dbd0 --- /dev/null +++ b/database/migrations/2019_03_26_224641_create_likes_table.php @@ -0,0 +1,37 @@ +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'); + } +} diff --git a/database/migrations/2019_04_09_224506_add_private_likes_to_users.php b/database/migrations/2019_04_09_224506_add_private_likes_to_users.php new file mode 100644 index 0000000..c2147bf --- /dev/null +++ b/database/migrations/2019_04_09_224506_add_private_likes_to_users.php @@ -0,0 +1,32 @@ +boolean('private_likes')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('private_likes'); + }); + } +} diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 6af9993..bf79d54 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -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('いいねできませんでした。'); + }); + } + }); }); \ No newline at end of file diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index e691a70..928bd95 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -12,4 +12,5 @@ $primary: #e53fb1; @import "tissue.css"; // Components -@import "components/link-card"; \ No newline at end of file +@import "components/ejaculation"; +@import "components/link-card"; diff --git a/resources/assets/sass/components/_ejaculation.scss b/resources/assets/sass/components/_ejaculation.scss new file mode 100644 index 0000000..dfe6e82 --- /dev/null +++ b/resources/assets/sass/components/_ejaculation.scss @@ -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; +} \ No newline at end of file diff --git a/resources/assets/sass/tissue.css b/resources/assets/sass/tissue.css index 2dd99e2..c8d5f15 100644 --- a/resources/assets/sass/tissue.css +++ b/resources/assets/sass/tissue.css @@ -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); diff --git a/resources/views/components/ejaculation.blade.php b/resources/views/components/ejaculation.blade.php new file mode 100644 index 0000000..7b76bcf --- /dev/null +++ b/resources/views/components/ejaculation.blade.php @@ -0,0 +1,53 @@ + +
+ @foreach ($ejaculation->tags as $tag) + {{ $tag->name }} + @endforeach +
+@endif + +@if (!empty($ejaculation->link)) ++ {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} +
+@endif + +@if ($ejaculation->likes_count > 0) ++
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
@endif + + @if ($ejaculation->likes_count > 0) +- @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent- @if ($ejaculation->is_private) - 非公開 - @endif - @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent- @if ($ejaculation->is_private) - 非公開 - @endif - @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent+ このユーザはいいね一覧を公開していません。 +
+@else +まだ何もいいと思ったことがありません。
++
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
@endif + + @if ($ejaculation->likes_count > 0) +