Skip to content

Commit

Permalink
Merge pull request #368 from pacoorozco/issue-359
Browse files Browse the repository at this point in the history
Implement notifications for unlocked badges
  • Loading branch information
pacoorozco authored Oct 12, 2022
2 parents 1a988c2 + 66e129d commit 0ff27d7
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 8 deletions.
50 changes: 50 additions & 0 deletions app/Http/Controllers/MarkNotificationAsReadController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/**
* Gamify - Gamification platform to implement any serious game mechanic.
*
* Copyright (c) 2018 by Paco Orozco <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Some rights reserved. See LICENSE and AUTHORS files.
*
* @author Paco Orozco <[email protected]>
* @copyright 2018 Paco Orozco
* @license GPL-3.0 <http://spdx.org/licenses/GPL-3.0>
*
* @link https://github.com/pacoorozco/gamify-laravel
*/

namespace Gamify\Http\Controllers;

use Gamify\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;

class MarkNotificationAsReadController extends Controller
{
public function __invoke(Request $request): Response
{
/** @var User $user */
$user = User::findOrFail(Auth::id());

$user
->unreadNotifications
->when($request->input('id'), function ($query) use ($request) {
return $query->where('id', $request->input('id'));
})
->markAsRead();

return response()
->noContent();
}
}
3 changes: 3 additions & 0 deletions app/Libs/Game/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

use Gamify\Models\Badge;
use Gamify\Models\User;
use Gamify\Notifications\BadgeUnlocked;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;

Expand Down Expand Up @@ -99,6 +100,8 @@ public static function unlockBadgeFor(User $user, Badge $badge): void
// this is the first occurrence of this badge for this user
$user->badges()->attach($badge->id, $data);
}

$user->notify(new BadgeUnlocked($badge));
}

public static function getTopExperiencedPlayers(int $numberOfPlayers = 10): \Illuminate\Support\Collection
Expand Down
1 change: 1 addition & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
* @property int $experience The reputation of the user.
* @property UserProfile $profile The user's profile
* @property-read string $level The current level of the user.
* @property-read Collection $unreadNotifications The user's unread notifications.
*/
final class User extends Authenticatable implements MustVerifyEmail
{
Expand Down
36 changes: 36 additions & 0 deletions app/Notifications/BadgeUnlocked.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Gamify\Notifications;

use Gamify\Models\Badge;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class BadgeUnlocked extends Notification
{
use Queueable;

public function __construct(
private Badge $badge,
) {
//
}

public function via(mixed $notifiable): array
{
return ['database'];
}

public function toDatabase(mixed $notifiable): array
{
return $this->toArray($notifiable);
}

public function toArray(mixed $notifiable): array
{
return [
'title' => __('notifications.badge_unlocked_title'),
'message' => __('notifications.badge_unlocked_message', ['name' => $this->badge->name, 'url' => route('account.index')]),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('notifications');
}
};
44 changes: 44 additions & 0 deletions lang/en/notifications.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/**
* Gamify - Gamification platform to implement any serious game mechanic.
*
* Copyright (c) 2018 by Paco Orozco <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Some rights reserved. See LICENSE and AUTHORS files.
*
* @author Paco Orozco <[email protected]>
* @copyright 2018 Paco Orozco
* @license GPL-3.0 <http://spdx.org/licenses/GPL-3.0>
*
* @link https://github.com/pacoorozco/gamify-laravel
*/

return [

/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/

'mark_as_read' => 'Mark as read',

'badge_unlocked_title' => 'Badge unlocked',
'badge_unlocked_message' => 'Congrats! You unlocked the badge \'<strong>:name</strong>\'. Visit <a href=":url">your profile</a> to check it.',

];
4 changes: 0 additions & 4 deletions resources/views/account/password/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@
@endsection

@section('content')
<!-- Notifications -->
@include('partials.notifications')
<!-- ./ notifications -->

<!-- panel -->
<div class="panel panel-default">
<div class="panel-body">
Expand Down
3 changes: 3 additions & 0 deletions resources/views/layouts/master.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@

<!-- start: PAGE CONTENT -->
<section class="content">

@include('partials.notifications')

@yield('content')
</section>
<!-- end: PAGE CONTENT-->
Expand Down
44 changes: 44 additions & 0 deletions resources/views/partials/notifications.blade.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
@auth
@foreach (Auth::user()->unreadNotifications as $notification)
<div class="alert alert-success alert-dismissible" role="alert">
<strong>
{{ __('notifications.badge_unlocked_title') }}
<small class="pull-right time">
<i class="fa fa-clock-o"></i>
{{ $notification->created_at->diffForHumans() }}
</small>
</strong>
<p>
{!! $notification->data['message'] !!}
<a href="#" class="pull-right mark-as-read" data-id="{{ $notification->id }}" data-dismiss="alert">
{{ __('notifications.mark_as_read') }}
</a>
</p>
</div>
@endforeach

@pushonce('scripts')
<script>
function sendMarkRequest(id = null) {
return $.ajax({
url: "{{ route('notifications.read') }}",
method: 'POST',
data: {
_token: '{{ csrf_token() }}',
_method: 'PATCH',
id: id
}
});
}
$(function () {
$('.mark-as-read').click(function () {
sendMarkRequest($(this).data('id'));
});
});
</script>
@endpushonce
@endauth

@if (session()->has('success'))
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
Expand Down Expand Up @@ -46,3 +88,5 @@
</ul>
</div>
@endif


2 changes: 1 addition & 1 deletion resources/views/partials/sidebar.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<ul class="nav navbar-nav">

@auth
<li class="{{ request()->is('dashboard') ? 'active' : '' }}">
<a href="{{ route('dashboard') }}">
{{ __('site.home') }}
Expand All @@ -9,7 +10,6 @@
</a>
</li>

@auth
<li class="{{ request()->is('questions*') ? 'active' : '' }}">
<a href="{{ route('questions.index') }}">
{{ __('site.play') }}
Expand Down
3 changes: 0 additions & 3 deletions resources/views/profile/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@

</div>
<div class="col-md-8">
<!-- notifications -->
@include('partials.notifications')
<!-- ./notifications -->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li class="active">
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use Gamify\Http\Controllers\Admin\AdminUserDataTablesController;
use Gamify\Http\Controllers\HomeController;
use Gamify\Http\Controllers\LeaderBoardController;
use Gamify\Http\Controllers\MarkNotificationAsReadController;
use Gamify\Http\Controllers\QuestionController;
use Gamify\Http\Controllers\ShowUserProfileController;
use Illuminate\Support\Facades\Route;
Expand Down Expand Up @@ -90,6 +91,9 @@
Route::post('password', [ChangePasswordController::class, 'update'])
->name('account.password.update');
});

Route::patch('notifications', MarkNotificationAsReadController::class)
->name('notifications.read');
});

/* ------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* Gamify - Gamification platform to implement any serious game mechanic.
*
* Copyright (c) 2018 by Paco Orozco <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Some rights reserved. See LICENSE and AUTHORS files.
*
* @author Paco Orozco <[email protected]>
* @copyright 2018 Paco Orozco
* @license GPL-3.0 <http://spdx.org/licenses/GPL-3.0>
*
* @link https://github.com/pacoorozco/gamify-laravel
*/

namespace Tests\Feature\Http\Controllers;

use Gamify\Models\Badge;
use Gamify\Models\User;
use Gamify\Notifications\BadgeUnlocked;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class MarkNotificationAsReadControllerTest extends TestCase
{
use RefreshDatabase;

/** @test */
public function it_should_mark_a_notification_as_read(): void
{
/** @var User $user */
$user = User::factory()->create();

/** @var Badge $badge */
$badge = Badge::factory()->create();

// Let's add two notifications.
$user->notify(new BadgeUnlocked($badge));
$user->notify(new BadgeUnlocked($badge));

// We want to mark only one as read.
$notification = $user->unreadNotifications->first();

$this
->actingAs($user)
->patch(route('notifications.read'), ['id' => $notification->id])
->assertNoContent();

$this->assertCount(1, $user->refresh()->unreadNotifications);
}

/** @test */
public function it_should_mark_all_notifications_as_read(): void
{
/** @var User $user */
$user = User::factory()->create();

/** @var Badge $badge */
$badge = Badge::factory()->create();

// Let's add two notifications.
$user->notify(new BadgeUnlocked($badge));
$user->notify(new BadgeUnlocked($badge));

$this
->actingAs($user)
->patch(route('notifications.read'))
->assertNoContent();

$this->assertCount(0, $user->refresh()->unreadNotifications);
}
}

0 comments on commit 0ff27d7

Please sign in to comment.