progress
This commit is contained in:
@@ -8,33 +8,36 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Redirect;
|
use Illuminate\Support\Facades\Redirect;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display the user's profile form.
|
* Display the user's profile form.
|
||||||
*/
|
*/
|
||||||
public function edit(Request $request): View
|
public function edit(): View
|
||||||
{
|
{
|
||||||
return view('profile.edit', [
|
return view('profile.edit', [
|
||||||
'user' => $request->user(),
|
'user' => Auth::user()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the user's profile information.
|
* Update the user's profile information.
|
||||||
*/
|
*/
|
||||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->user()->fill($request->validated());
|
$user = Auth::user();
|
||||||
|
|
||||||
if ($request->user()->isDirty('email')) {
|
$validated = $request->validate([
|
||||||
$request->user()->email_verified_at = null;
|
'name' => ['required', 'string', 'max:255'],
|
||||||
}
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
|
||||||
|
]);
|
||||||
|
|
||||||
$request->user()->save();
|
$user->update($validated);
|
||||||
|
|
||||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
return redirect()->route('profile.show')
|
||||||
|
->with('success', 'Profile updated successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +58,59 @@ class ProfileController extends Controller
|
|||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
return Redirect::to('/');
|
return redirect()->to('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(User $user = null): View
|
||||||
|
{
|
||||||
|
$user = $user ?? Auth::user();
|
||||||
|
$recentActivity = collect();
|
||||||
|
|
||||||
|
// Get recent reviews
|
||||||
|
$recentReviews = $user->reviews()
|
||||||
|
->with('game')
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get recent ratings
|
||||||
|
$recentRatings = $user->ratings()
|
||||||
|
->with('game')
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Get recent lists
|
||||||
|
$recentLists = $user->lists()
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Combine and sort all activity
|
||||||
|
$recentActivity = $recentActivity
|
||||||
|
->concat($recentReviews)
|
||||||
|
->concat($recentRatings)
|
||||||
|
->concat($recentLists)
|
||||||
|
->sortByDesc('created_at')
|
||||||
|
->take(10);
|
||||||
|
|
||||||
|
return view('profile.show', [
|
||||||
|
'user' => $user,
|
||||||
|
'recentActivity' => $recentActivity
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviews(): View
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$reviews = $user->reviews()
|
||||||
|
->with('game')
|
||||||
|
->latest()
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return view('profile.reviews', [
|
||||||
|
'user' => $user,
|
||||||
|
'reviews' => $reviews
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-17
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,7 @@ class User extends Authenticatable
|
|||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
'slug',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
];
|
];
|
||||||
@@ -44,6 +47,38 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($user) {
|
||||||
|
$user->slug = Str::slug($user->name);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updating(function ($user) {
|
||||||
|
if ($user->isDirty('name')) {
|
||||||
|
$user->slug = Str::slug($user->name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::created(function ($user) {
|
||||||
|
// Create default lists
|
||||||
|
$defaultLists = [
|
||||||
|
['name' => 'Played Games', 'is_default' => true],
|
||||||
|
['name' => 'Want to Play', 'is_default' => true]
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($defaultLists as $list) {
|
||||||
|
$user->lists()->create($list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRouteKeyName()
|
||||||
|
{
|
||||||
|
return 'slug';
|
||||||
|
}
|
||||||
|
|
||||||
public function reviews(): HasMany
|
public function reviews(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Review::class);
|
return $this->hasMany(Review::class);
|
||||||
@@ -58,21 +93,4 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(GameList::class);
|
return $this->hasMany(GameList::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function boot()
|
|
||||||
{
|
|
||||||
parent::boot();
|
|
||||||
|
|
||||||
static::created(function ($user) {
|
|
||||||
// Create default lists
|
|
||||||
$defaultLists = [
|
|
||||||
['name' => 'Played Games', 'is_default' => true],
|
|
||||||
['name' => 'Want to Play', 'is_default' => true]
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($defaultLists as $list) {
|
|
||||||
$user->lists()->create($list);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('slug')->unique()->after('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('slug');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
+19
-1
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "cleargame",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"star-rating.js": "^4.3.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
@@ -777,6 +780,12 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-it": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-it/-/detect-it-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-dg5YBTJYvogK1+dA2mBUDKzOWfYZtHVba89SyZUhc4+e3i2tzgjANFg5lDRCd3UOtRcw00vUTMK8LELcMdicug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1214,6 +1223,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/star-rating.js": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/star-rating.js/-/star-rating.js-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynf+5eyXIn1HKCoKy5gc5Cmu8l1mvw+bYhogzEMLNqZ2AAh7MLP51ZmJnt98/AzO3DZ5h2i/RgkbVPlBhDGebQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-it": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.18",
|
"version": "5.4.18",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
|
||||||
|
|||||||
@@ -11,5 +11,8 @@
|
|||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"star-rating.js": "^4.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Star Rating
|
||||||
|
* @version: 4.3.0
|
||||||
|
* @author: Paul Ryley (http://geminilabs.io)
|
||||||
|
* @url: https://github.com/pryley/star-rating.js
|
||||||
|
* @license: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--gl-star-color: #fdd835;
|
||||||
|
--gl-star-color-inactive: #dcdce6;
|
||||||
|
--gl-star-empty: url('../img/star-empty.svg');
|
||||||
|
--gl-star-full: url('../img/star-full.svg');
|
||||||
|
--gl-star-size: 24px;
|
||||||
|
--gl-tooltip-background: rgba(17,17,17, .9);
|
||||||
|
--gl-tooltip-border-radius: 4px;
|
||||||
|
--gl-tooltip-color: #fff;
|
||||||
|
--gl-tooltip-font-size: 0.875rem;
|
||||||
|
--gl-tooltip-font-weight: 400;
|
||||||
|
--gl-tooltip-line-height: 1;
|
||||||
|
--gl-tooltip-margin: 12px;
|
||||||
|
--gl-tooltip-padding: .5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] > select {
|
||||||
|
-webkit-clip-path: circle(1px at 0 0) !important;
|
||||||
|
clip-path: circle(1px at 0 0) !important;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||||
|
height: 1px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] > select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] > select::before,
|
||||||
|
[data-star-rating] > select::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating].gl-star-rating--ltr > select {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating].gl-star-rating--rtl > select {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-star-rating:not([data-star-rating]) .gl-star-rating--stars {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-rating--stars {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] > select:focus + .gl-star-rating--stars span:first-child::before {
|
||||||
|
box-shadow: 0 0 0 3px -moz-mac-focusring;
|
||||||
|
box-shadow: 0 0 0 3px -webkit-focus-ring-color;
|
||||||
|
box-shadow: 0 0 0 3px Highlight;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] select[disabled] + .gl-star-rating--stars {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-rating--stars > span {
|
||||||
|
display: flex;
|
||||||
|
height: 24px;
|
||||||
|
height: 24px;
|
||||||
|
height: var(--gl-star-size);
|
||||||
|
margin: 0;
|
||||||
|
width: 24px;
|
||||||
|
width: 24px;
|
||||||
|
width: var(--gl-star-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-rating--stars[aria-label]::before,
|
||||||
|
[data-star-rating] .gl-star-rating--stars[aria-label]::after {
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
bottom: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform-origin: top;
|
||||||
|
transform: translate3d(0,-50%,0);
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-rating--stars[aria-label]::before {
|
||||||
|
background: rgba(17,17,17, .9);
|
||||||
|
background: rgba(17,17,17, .9);
|
||||||
|
background: var(--gl-tooltip-background);
|
||||||
|
-webkit-clip-path: path('M6 14.998c0-3-6-5.499-6-7.499S5.999 3 5.999 0L6 14.998z');
|
||||||
|
clip-path: path('M6 14.998c0-3-6-5.499-6-7.499S5.999 3 5.999 0L6 14.998z');
|
||||||
|
content: '';
|
||||||
|
height: 15px;
|
||||||
|
margin: 0 0 0 6px;
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-rating--stars[aria-label]::after {
|
||||||
|
background: rgba(17,17,17, .9);
|
||||||
|
background: rgba(17,17,17, .9);
|
||||||
|
background: var(--gl-tooltip-background);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-radius: var(--gl-tooltip-border-radius);
|
||||||
|
color: #fff;
|
||||||
|
color: #fff;
|
||||||
|
color: var(--gl-tooltip-color);
|
||||||
|
content: attr(aria-label);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-size: var(--gl-tooltip-font-size);
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-left: var(--gl-tooltip-margin);
|
||||||
|
padding: .5em 1em;
|
||||||
|
padding: .5em 1em;
|
||||||
|
padding: var(--gl-tooltip-padding);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating].gl-star-rating--rtl .gl-star-rating--stars[aria-label]::before,
|
||||||
|
[data-star-rating].gl-star-rating--rtl .gl-star-rating--stars[aria-label]::after {
|
||||||
|
left: auto;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating].gl-star-rating--rtl .gl-star-rating--stars[aria-label]::before {
|
||||||
|
transform: scaleX(-1) translate3d(0,-50%,0);
|
||||||
|
margin: 0 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating].gl-star-rating--rtl .gl-star-rating--stars[aria-label]::after {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-right: var(--gl-tooltip-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] svg {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-half {
|
||||||
|
fill: none;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-star-full {
|
||||||
|
fill: #dcdce6;
|
||||||
|
fill: #dcdce6;
|
||||||
|
fill: var(--gl-star-color-inactive);
|
||||||
|
stroke: #dcdce6;
|
||||||
|
stroke: #dcdce6;
|
||||||
|
stroke: var(--gl-star-color-inactive);
|
||||||
|
transition: fill 0.15s ease-in-out, stroke 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-star-rating] .gl-active .gl-star-full {
|
||||||
|
fill: #fdd835;
|
||||||
|
fill: #fdd835;
|
||||||
|
fill: var(--gl-star-color);
|
||||||
|
stroke: #fdd835;
|
||||||
|
stroke: #fdd835;
|
||||||
|
stroke: var(--gl-star-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compatibilty with v3 */
|
||||||
|
|
||||||
|
.gl-star-rating--stars[class*=" s"] > span {
|
||||||
|
background-image: url('../img/star-empty.svg') !important;
|
||||||
|
background-image: url('../img/star-empty.svg') !important;
|
||||||
|
background-image: var(--gl-star-empty) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-star-rating--stars[class*=" s"] > span {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-star-rating--stars[class*=" s"] > span.gl-active,
|
||||||
|
.gl-star-rating--stars[class*=" s"] > span.gl-active.gl-selected {
|
||||||
|
background-image: url('../img/star-full.svg') !important;
|
||||||
|
background-image: url('../img/star-full.svg') !important;
|
||||||
|
background-image: var(--gl-star-full) !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
var StarRating = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function _classCallCheck(instance, Constructor) {
|
||||||
|
if (!(instance instanceof Constructor)) {
|
||||||
|
throw new TypeError("Cannot call a class as a function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _defineProperties(target, props) {
|
||||||
|
for (var i = 0; i < props.length; i++) {
|
||||||
|
var descriptor = props[i];
|
||||||
|
descriptor.enumerable = descriptor.enumerable || false;
|
||||||
|
descriptor.configurable = true;
|
||||||
|
if ("value" in descriptor) descriptor.writable = true;
|
||||||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _createClass(Constructor, protoProps, staticProps) {
|
||||||
|
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||||
|
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||||
|
return Constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
classNames: {
|
||||||
|
active: 'gl-active',
|
||||||
|
base: 'gl-star-rating',
|
||||||
|
selected: 'gl-selected'
|
||||||
|
},
|
||||||
|
clearable: true,
|
||||||
|
maxStars: 10,
|
||||||
|
prebuilt: false,
|
||||||
|
stars: null,
|
||||||
|
tooltip: 'Select a Rating'
|
||||||
|
};
|
||||||
|
|
||||||
|
var addRemoveClass = function addRemoveClass(el, bool, className) {
|
||||||
|
el.classList[bool ? 'add' : 'remove'](className);
|
||||||
|
};
|
||||||
|
var createSpanEl = function createSpanEl(attributes) {
|
||||||
|
var el = document.createElement('span');
|
||||||
|
attributes = attributes || {};
|
||||||
|
|
||||||
|
for (var key in attributes) {
|
||||||
|
el.setAttribute(key, attributes[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
var inRange = function inRange(value, min, max) {
|
||||||
|
return /^\d+$/.test(value) && min <= value && value <= max;
|
||||||
|
};
|
||||||
|
var insertSpanEl = function insertSpanEl(el, after, attributes) {
|
||||||
|
var newEl = createSpanEl(attributes);
|
||||||
|
el.parentNode.insertBefore(newEl, after ? el.nextSibling : el);
|
||||||
|
return newEl;
|
||||||
|
};
|
||||||
|
var merge = function merge() {
|
||||||
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||||
|
args[_key] = arguments[_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://github.com/firstandthird/aug
|
||||||
|
var results = {};
|
||||||
|
args.forEach(function (prop) {
|
||||||
|
Object.keys(prop || {}).forEach(function (propName) {
|
||||||
|
if (args[0][propName] === undefined) return; // restrict keys to the defaults
|
||||||
|
|
||||||
|
var propValue = prop[propName];
|
||||||
|
|
||||||
|
if (type(propValue) === 'Object' && type(results[propName]) === 'Object') {
|
||||||
|
results[propName] = merge(results[propName], propValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results[propName] = propValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var type = function type(value) {
|
||||||
|
return {}.toString.call(value).slice(8, -1);
|
||||||
|
};
|
||||||
|
var values = function values(selectEl) {
|
||||||
|
var values = [];
|
||||||
|
[].forEach.call(selectEl.options, function (el) {
|
||||||
|
var value = parseInt(el.value, 10) || 0;
|
||||||
|
|
||||||
|
if (value > 0) {
|
||||||
|
values.push({
|
||||||
|
index: el.index,
|
||||||
|
text: el.text,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return values.sort(function (a, b) {
|
||||||
|
return a.value - b.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var Widget = /*#__PURE__*/function () {
|
||||||
|
function Widget(el, props) {
|
||||||
|
_classCallCheck(this, Widget);
|
||||||
|
|
||||||
|
// (HTMLElement, object):void
|
||||||
|
this.direction = window.getComputedStyle(el, null).getPropertyValue('direction');
|
||||||
|
this.el = el;
|
||||||
|
this.events = {
|
||||||
|
change: this.onChange.bind(this),
|
||||||
|
keydown: this.onKeyDown.bind(this),
|
||||||
|
mousedown: this.onPointerDown.bind(this),
|
||||||
|
mouseleave: this.onPointerLeave.bind(this),
|
||||||
|
mousemove: this.onPointerMove.bind(this),
|
||||||
|
reset: this.onReset.bind(this),
|
||||||
|
touchend: this.onPointerDown.bind(this),
|
||||||
|
touchmove: this.onPointerMove.bind(this)
|
||||||
|
};
|
||||||
|
this.indexActive = null; // the active span index
|
||||||
|
|
||||||
|
this.indexSelected = null; // the selected span index
|
||||||
|
|
||||||
|
this.props = props;
|
||||||
|
this.tick = null;
|
||||||
|
this.ticking = false;
|
||||||
|
this.values = values(el);
|
||||||
|
this.widgetEl = null;
|
||||||
|
|
||||||
|
if (this.el.widget) {
|
||||||
|
this.el.widget.destroy(); // remove any stale event listeners
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inRange(this.values.length, 1, this.props.maxStars)) {
|
||||||
|
this.build();
|
||||||
|
} else {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createClass(Widget, [{
|
||||||
|
key: "build",
|
||||||
|
value: function build() {
|
||||||
|
// ():void
|
||||||
|
this.destroy();
|
||||||
|
this.buildWidget();
|
||||||
|
this.selectValue(this.indexSelected = this.selected(), false); // set the initial value but do not trigger change event
|
||||||
|
|
||||||
|
this.handleEvents('add');
|
||||||
|
this.el.widget = this; // store a reference to this widget on the SELECT so that we can remove stale event listeners
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "buildWidget",
|
||||||
|
value: function buildWidget() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
// ():void
|
||||||
|
var parentEl = null;
|
||||||
|
var widgetEl = null;
|
||||||
|
|
||||||
|
if (this.props.prebuilt) {
|
||||||
|
parentEl = this.el.parentNode;
|
||||||
|
widgetEl = parentEl.querySelector('.' + this.props.classNames.base + '--stars');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === widgetEl) {
|
||||||
|
parentEl = insertSpanEl(this.el, false, {
|
||||||
|
"class": this.props.classNames.base
|
||||||
|
});
|
||||||
|
parentEl.appendChild(this.el);
|
||||||
|
widgetEl = insertSpanEl(this.el, true, {
|
||||||
|
"class": this.props.classNames.base + '--stars'
|
||||||
|
});
|
||||||
|
this.values.forEach(function (item, index) {
|
||||||
|
var el = createSpanEl({
|
||||||
|
'data-index': index,
|
||||||
|
'data-value': item.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('function' === typeof _this.props.stars) {
|
||||||
|
_this.props.stars.call(_this, el, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
[].forEach.call(el.children, function (el) {
|
||||||
|
return el.style.pointerEvents = 'none';
|
||||||
|
});
|
||||||
|
widgetEl.innerHTML += el.outerHTML;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parentEl.dataset.starRating = '';
|
||||||
|
parentEl.classList.add(this.props.classNames.base + '--' + this.direction);
|
||||||
|
|
||||||
|
if (this.props.tooltip) {
|
||||||
|
widgetEl.setAttribute('role', 'tooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.widgetEl = widgetEl;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "changeIndexTo",
|
||||||
|
value: function changeIndexTo(index, force) {
|
||||||
|
var _this2 = this;
|
||||||
|
|
||||||
|
// (int):void
|
||||||
|
if (this.indexActive !== index || force) {
|
||||||
|
[].forEach.call(this.widgetEl.children, function (el, i) {
|
||||||
|
// i starts at zero
|
||||||
|
addRemoveClass(el, i <= index, _this2.props.classNames.active);
|
||||||
|
addRemoveClass(el, i === _this2.indexSelected, _this2.props.classNames.selected);
|
||||||
|
});
|
||||||
|
this.widgetEl.setAttribute('data-rating', index + 1);
|
||||||
|
|
||||||
|
if ('function' !== typeof this.props.stars && !this.props.prebuilt) {
|
||||||
|
// @v3 compat
|
||||||
|
this.widgetEl.classList.remove('s' + 10 * (this.indexActive + 1));
|
||||||
|
this.widgetEl.classList.add('s' + 10 * (index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.tooltip) {
|
||||||
|
var _this$values$index;
|
||||||
|
|
||||||
|
var label = index < 0 ? this.props.tooltip : (_this$values$index = this.values[index]) === null || _this$values$index === void 0 ? void 0 : _this$values$index.text;
|
||||||
|
this.widgetEl.setAttribute('aria-label', label);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.indexActive = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ticking = false;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "destroy",
|
||||||
|
value: function destroy() {
|
||||||
|
// ():void
|
||||||
|
this.indexActive = null; // the active span index
|
||||||
|
|
||||||
|
this.indexSelected = this.selected(); // the selected span index
|
||||||
|
|
||||||
|
var parentEl = this.el.parentNode;
|
||||||
|
|
||||||
|
if (parentEl.classList.contains(this.props.classNames.base)) {
|
||||||
|
if (this.props.prebuilt) {
|
||||||
|
this.widgetEl = parentEl.querySelector('.' + this.props.classNames.base + '--stars');
|
||||||
|
parentEl.classList.remove(this.props.classNames.base + '--' + this.direction);
|
||||||
|
delete parentEl.dataset.starRating;
|
||||||
|
} else {
|
||||||
|
parentEl.parentNode.replaceChild(this.el, parentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleEvents('remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.el.widget; // remove the widget reference
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "eventListener",
|
||||||
|
value: function eventListener(el, action, events, items) {
|
||||||
|
var _this3 = this;
|
||||||
|
|
||||||
|
// (HTMLElement, string, array, object):void
|
||||||
|
events.forEach(function (ev) {
|
||||||
|
return el[action + 'EventListener'](ev, _this3.events[ev], items || false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "handleEvents",
|
||||||
|
value: function handleEvents(action) {
|
||||||
|
// (string):void
|
||||||
|
var formEl = this.el.closest('form');
|
||||||
|
|
||||||
|
if (formEl && formEl.tagName === 'FORM') {
|
||||||
|
this.eventListener(formEl, action, ['reset']);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventListener(this.el, action, ['change']); // always trigger the change event, even when SELECT is disabled
|
||||||
|
|
||||||
|
if ('add' === action && this.el.disabled) return;
|
||||||
|
this.eventListener(this.el, action, ['keydown']);
|
||||||
|
this.eventListener(this.widgetEl, action, ['mousedown', 'mouseleave', 'mousemove', 'touchend', 'touchmove'], false);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "indexFromEvent",
|
||||||
|
value: function indexFromEvent(ev) {
|
||||||
|
var _ev$touches, _ev$changedTouches;
|
||||||
|
|
||||||
|
// (MouseEvent|TouchEvent):void
|
||||||
|
var origin = ((_ev$touches = ev.touches) === null || _ev$touches === void 0 ? void 0 : _ev$touches[0]) || ((_ev$changedTouches = ev.changedTouches) === null || _ev$changedTouches === void 0 ? void 0 : _ev$changedTouches[0]) || ev;
|
||||||
|
var el = document.elementFromPoint(origin.clientX, origin.clientY);
|
||||||
|
|
||||||
|
if (el.parentNode === this.widgetEl) {
|
||||||
|
return [].slice.call(el.parentNode.children).indexOf(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.indexActive;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onChange",
|
||||||
|
value: function onChange() {
|
||||||
|
// ():void
|
||||||
|
this.changeIndexTo(this.selected(), true);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onKeyDown",
|
||||||
|
value: function onKeyDown(ev) {
|
||||||
|
// (KeyboardEvent):void
|
||||||
|
var key = ev.key.slice(5);
|
||||||
|
if (!~['Left', 'Right'].indexOf(key)) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
var increment = key === 'Left' ? -1 : 1;
|
||||||
|
|
||||||
|
if (this.direction === 'rtl') {
|
||||||
|
increment *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxIndex = this.values.length - 1;
|
||||||
|
var minIndex = -1;
|
||||||
|
var index = Math.min(Math.max(this.selected() + increment, minIndex), maxIndex);
|
||||||
|
this.selectValue(index, true); // trigger change event
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onPointerDown",
|
||||||
|
value: function onPointerDown(ev) {
|
||||||
|
// (MouseEvent|TouchEvent):void
|
||||||
|
ev.preventDefault(); // this.el.focus(); // highlight the rating field
|
||||||
|
|
||||||
|
var index = this.indexFromEvent(ev);
|
||||||
|
|
||||||
|
if (this.props.clearable && index === this.indexSelected) {
|
||||||
|
index = -1; // remove the value
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectValue(index, true); // trigger change event
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onPointerLeave",
|
||||||
|
value: function onPointerLeave(ev) {
|
||||||
|
var _this4 = this;
|
||||||
|
|
||||||
|
// (MouseEvent):void
|
||||||
|
ev.preventDefault();
|
||||||
|
cancelAnimationFrame(this.tick);
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
return _this4.changeIndexTo(_this4.indexSelected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onPointerMove",
|
||||||
|
value: function onPointerMove(ev) {
|
||||||
|
var _this5 = this;
|
||||||
|
|
||||||
|
// (MouseEvent|TouchEvent):void
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (!this.ticking) {
|
||||||
|
this.tick = requestAnimationFrame(function () {
|
||||||
|
return _this5.changeIndexTo(_this5.indexFromEvent(ev));
|
||||||
|
});
|
||||||
|
this.ticking = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "onReset",
|
||||||
|
value: function onReset() {
|
||||||
|
var _this$el$querySelecto;
|
||||||
|
|
||||||
|
// ():void
|
||||||
|
var index = this.valueIndex((_this$el$querySelecto = this.el.querySelector('[selected]')) === null || _this$el$querySelecto === void 0 ? void 0 : _this$el$querySelecto.value);
|
||||||
|
this.selectValue(index || -1, false); // do not trigger change event
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "selected",
|
||||||
|
value: function selected() {
|
||||||
|
// ():int
|
||||||
|
return this.valueIndex(this.el.value); // get the selected span index
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "selectValue",
|
||||||
|
value: function selectValue(index, triggerChangeEvent) {
|
||||||
|
var _this$values$index2;
|
||||||
|
|
||||||
|
// (int, bool):void
|
||||||
|
this.el.value = ((_this$values$index2 = this.values[index]) === null || _this$values$index2 === void 0 ? void 0 : _this$values$index2.value) || ''; // first set the new value
|
||||||
|
|
||||||
|
this.indexSelected = this.selected(); // get the actual index from the selected value
|
||||||
|
|
||||||
|
if (false === triggerChangeEvent) {
|
||||||
|
this.changeIndexTo(this.selected(), true);
|
||||||
|
} else {
|
||||||
|
this.el.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "valueIndex",
|
||||||
|
value: function valueIndex(value) {
|
||||||
|
return this.values.findIndex(function (val) {
|
||||||
|
return val.value === +value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
return Widget;
|
||||||
|
}();
|
||||||
|
|
||||||
|
var StarRating = /*#__PURE__*/function () {
|
||||||
|
function StarRating(selector, props) {
|
||||||
|
_classCallCheck(this, StarRating);
|
||||||
|
|
||||||
|
// (HTMLSelectElement|NodeList|string, object):void
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
this.props = props;
|
||||||
|
this.rebuild = this.rebuild.bind(this);
|
||||||
|
this.selector = selector;
|
||||||
|
this.widgets = [];
|
||||||
|
this.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createClass(StarRating, [{
|
||||||
|
key: "build",
|
||||||
|
value: function build() {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
// (HTMLSelectElement|NodeList|string, object):void
|
||||||
|
this.queryElements(this.selector).forEach(function (el) {
|
||||||
|
var options = merge(defaults, _this.props, JSON.parse(el.getAttribute('data-options')));
|
||||||
|
|
||||||
|
if ('SELECT' === el.tagName && !el.widget) {
|
||||||
|
// check for an existing Widget reference
|
||||||
|
if (!options.prebuilt && el.parentNode.classList.contains(options.classNames.base)) {
|
||||||
|
_this.unwrap(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.widgets.push(new Widget(el, options));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "destroy",
|
||||||
|
value: function destroy() {
|
||||||
|
// ():void
|
||||||
|
this.widgets.forEach(function (widget) {
|
||||||
|
return widget.destroy();
|
||||||
|
});
|
||||||
|
this.widgets = [];
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "queryElements",
|
||||||
|
value: function queryElements(selector) {
|
||||||
|
// (HTMLSelectElement|NodeList|string):array
|
||||||
|
if ('HTMLSelectElement' === type(selector)) {
|
||||||
|
return [selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('NodeList' === type(selector)) {
|
||||||
|
return [].slice.call(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('String' === type(selector)) {
|
||||||
|
return [].slice.call(document.querySelectorAll(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "rebuild",
|
||||||
|
value: function rebuild() {
|
||||||
|
// ():void
|
||||||
|
this.destroy();
|
||||||
|
this.build();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: "unwrap",
|
||||||
|
value: function unwrap(el) {
|
||||||
|
var removeEl = el.parentNode;
|
||||||
|
var parentEl = removeEl.parentNode;
|
||||||
|
parentEl.insertBefore(el, removeEl);
|
||||||
|
parentEl.removeChild(removeEl);
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
return StarRating;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return StarRating;
|
||||||
|
|
||||||
|
}());
|
||||||
@@ -146,26 +146,64 @@
|
|||||||
<p class="text-muted mb-0">No reviews yet. Be the first to review this game!</p>
|
<p class="text-muted mb-0">No reviews yet. Be the first to review this game!</p>
|
||||||
@else
|
@else
|
||||||
@foreach($game->reviews as $review)
|
@foreach($game->reviews as $review)
|
||||||
<div class="mb-4 pb-4 border-bottom border-secondary">
|
<div class="card bg-dark text-white mb-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="card-header">
|
||||||
<div>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">{{ $review->user->name }}</h6>
|
<div class="d-flex align-items-center">
|
||||||
<small class="text-muted">{{ $review->created_at->diffForHumans() }}</small>
|
<div class="me-3">
|
||||||
</div>
|
<i class="fas fa-user-circle"></i>
|
||||||
<div class="d-flex align-items-center">
|
</div>
|
||||||
<div class="rating-stars d-flex gap-1 me-2">
|
<div>
|
||||||
@for($i = 1; $i <= 10; $i++)
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-star {{ $i <= $review->rating ? 'text-warning' : 'text-secondary' }}"></i>
|
<a href="{{ route('profile.show', $review->user) }}" class="text-white text-decoration-none">
|
||||||
@endfor
|
{{ $review->user->name }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-clock"></i> {{ $review->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if($review->recommended)
|
<div class="d-flex align-items-center">
|
||||||
<span class="badge bg-success">Recommended</span>
|
<div class="rating-stars d-flex gap-1 me-2">
|
||||||
@else
|
@for($i = 1; $i <= 10; $i++)
|
||||||
<span class="badge bg-danger">Not Recommended</span>
|
<i class="fas fa-star {{ $i <= $review->rating ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
@if($review->recommended)
|
||||||
|
<span class="badge bg-success">Recommended</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">Not Recommended</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="review-content">
|
||||||
|
{!! nl2br(e($review->content)) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('reviews.show', $review) }}" class="btn btn-link text-white p-0">
|
||||||
|
Read full review
|
||||||
|
</a>
|
||||||
|
@if(auth()->id() === $review->user_id)
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{{ route('reviews.edit', $review) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('reviews.destroy', $review) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Are you sure you want to delete this review?')">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-0">{{ $review->content }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,29 +1,48 @@
|
|||||||
<x-app-layout>
|
@extends('layouts.app')
|
||||||
<x-slot name="header">
|
|
||||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
|
||||||
{{ __('Profile') }}
|
|
||||||
</h2>
|
|
||||||
</x-slot>
|
|
||||||
|
|
||||||
<div class="py-12">
|
@section('title', 'Edit Profile')
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
|
||||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
@section('content')
|
||||||
<div class="max-w-xl">
|
<div class="container py-4">
|
||||||
@include('profile.partials.update-profile-information-form')
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Edit Profile</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card-body">
|
||||||
|
<form action="{{ route('profile.update') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
<div class="mb-4">
|
||||||
<div class="max-w-xl">
|
<label for="name" class="form-label">Name</label>
|
||||||
@include('profile.partials.update-password-form')
|
<input type="text" name="name" id="name" class="form-control bg-dark text-white @error('name') is-invalid @enderror" value="{{ old('name', $user->name) }}" required>
|
||||||
</div>
|
@error('name')
|
||||||
</div>
|
<div class="text-danger mt-1">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
<div class="mb-4">
|
||||||
<div class="max-w-xl">
|
<label for="email" class="form-label">Email</label>
|
||||||
@include('profile.partials.delete-user-form')
|
<input type="email" name="email" id="email" class="form-control bg-dark text-white @error('email') is-invalid @enderror" value="{{ old('email', $user->email) }}" required>
|
||||||
|
@error('email')
|
||||||
|
<div class="text-danger mt-1">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ route('profile.show') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-app-layout>
|
</div>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $user->name . "'s Reviews")
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">{{ $user->name }}'s Reviews</h1>
|
||||||
|
@if(auth()->id() === $user->id)
|
||||||
|
<a href="{{ route('profile.show', $user) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Profile
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($reviews->isEmpty())
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-comment-alt fa-3x mb-3"></i>
|
||||||
|
<h3>No Reviews Yet</h3>
|
||||||
|
<p class="text-muted">This user hasn't written any reviews yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="row">
|
||||||
|
@foreach($reviews as $review)
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card bg-dark text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<a href="{{ route('games.show', $review->game) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $review->game->title }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="badge bg-primary me-2">
|
||||||
|
<i class="fas fa-star"></i> {{ $review->rating }}/10
|
||||||
|
</span>
|
||||||
|
@if($review->recommended)
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="fas fa-thumbs-up"></i> Recommended
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<i class="fas fa-thumbs-down"></i> Not Recommended
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
<small>
|
||||||
|
<i class="far fa-clock"></i> {{ $review->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<p class="card-text">{{ Str::limit($review->content, 200) }}</p>
|
||||||
|
<a href="{{ route('reviews.show', $review) }}" class="btn btn-outline-primary">
|
||||||
|
Read Full Review
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center mt-4">
|
||||||
|
{{ $reviews->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $user->name . "'s Profile")
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<i class="fas fa-user-circle fa-5x"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title">{{ $user->name }}</h3>
|
||||||
|
<p class="text-muted">{{ $user->email }}</p>
|
||||||
|
@if(auth()->id() === $user->id)
|
||||||
|
<a href="{{ route('profile.edit') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Edit Profile
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Activity Summary</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Reviews</span>
|
||||||
|
<span class="badge bg-primary">{{ $user->reviews()->count() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>Ratings</span>
|
||||||
|
<span class="badge bg-primary">{{ $user->ratings()->count() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Lists</span>
|
||||||
|
<span class="badge bg-primary">{{ $user->lists()->count() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Recent Activity</h5>
|
||||||
|
@if(auth()->id() === $user->id)
|
||||||
|
<a href="{{ route('profile.reviews') }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-list"></i> View All Reviews
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if($recentActivity->isEmpty())
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-history fa-3x mb-3 text-muted"></i>
|
||||||
|
<p class="text-muted">No recent activity</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@foreach($recentActivity as $activity)
|
||||||
|
@if($activity instanceof \App\Models\Review)
|
||||||
|
<div class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">
|
||||||
|
<a href="{{ route('games.show', $activity->game) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $activity->game->title }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<p class="mb-1 text-muted">
|
||||||
|
<i class="fas fa-star text-warning"></i> Rated {{ $activity->rating }}/10
|
||||||
|
@if($activity->recommended)
|
||||||
|
<span class="badge bg-success ms-2">
|
||||||
|
<i class="fas fa-thumbs-up"></i> Recommended
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger ms-2">
|
||||||
|
<i class="fas fa-thumbs-down"></i> Not Recommended
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-clock"></i> {{ $activity->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('reviews.show', $activity) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
View Review
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif($activity instanceof \App\Models\Rating)
|
||||||
|
<div class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">
|
||||||
|
<a href="{{ route('games.show', $activity->game) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $activity->game->title }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<p class="mb-1 text-muted">
|
||||||
|
<i class="fas fa-star text-warning"></i> Rated {{ $activity->rating }}/10
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-clock"></i> {{ $activity->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif($activity instanceof \App\Models\GameList)
|
||||||
|
<div class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">
|
||||||
|
<a href="{{ route('lists.show', $activity) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $activity->name }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<p class="mb-1 text-muted">
|
||||||
|
<i class="fas fa-list"></i> {{ $activity->games()->count() }} games
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-clock"></i> {{ $activity->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('lists.show', $activity) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
View List
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -1,206 +1,101 @@
|
|||||||
@csrf
|
@extends('layouts.app')
|
||||||
|
|
||||||
<div class="mb-4">
|
@section('title', isset($review) ? 'Edit Review' : 'Write a Review')
|
||||||
<label for="content" class="form-label">Review</label>
|
|
||||||
<textarea name="content" id="content" rows="5"
|
|
||||||
class="form-control bg-dark text-white @error('content') is-invalid @enderror"
|
|
||||||
required>{{ old('content', $review->content ?? '') }}</textarea>
|
|
||||||
@error('content')
|
|
||||||
<div class="invalid-feedback">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
@section('content')
|
||||||
<label class="form-label">Rating</label>
|
<div class="container py-4">
|
||||||
<div class="rating-container">
|
<div class="row justify-content-center">
|
||||||
<div class="rating-stars d-flex gap-1">
|
<div class="col-md-8">
|
||||||
@for($i = 1; $i <= 10; $i++)
|
<div class="card bg-dark text-white">
|
||||||
<div class="rating-item" data-rating="{{ $i }}">
|
<div class="card-header">
|
||||||
<input type="radio" name="rating" id="rating{{ $i }}" value="{{ $i }}"
|
<h4 class="mb-0">{{ isset($review) ? 'Edit Review' : 'Write a Review' }}</h4>
|
||||||
class="rating-input" {{ old('rating', $review->rating ?? '') == $i ? 'checked' : '' }}>
|
|
||||||
<label for="rating{{ $i }}" class="rating-star">
|
|
||||||
<i class="fas fa-star"></i>
|
|
||||||
</label>
|
|
||||||
<span class="rating-number">{{ $i }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
@endfor
|
<div class="card-body">
|
||||||
</div>
|
<form action="{{ isset($review) ? route('reviews.update', $review) : route('games.reviews.store', $game) }}" method="POST">
|
||||||
<div class="rating-value mt-2 text-muted">
|
@csrf
|
||||||
<span class="current-rating">{{ old('rating', $review->rating ?? '0') }}</span>/10
|
@if(isset($review))
|
||||||
|
@method('PUT')
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="rating" class="form-label">Rating</label>
|
||||||
|
<select name="rating" id="rating" class="star-rating" required>
|
||||||
|
<option value="">Select a rating</option>
|
||||||
|
<option value="10" {{ isset($review) && $review->rating == 10 ? 'selected' : '' }}>10 - Masterpiece</option>
|
||||||
|
<option value="9" {{ isset($review) && $review->rating == 9 ? 'selected' : '' }}>9 - Amazing</option>
|
||||||
|
<option value="8" {{ isset($review) && $review->rating == 8 ? 'selected' : '' }}>8 - Great</option>
|
||||||
|
<option value="7" {{ isset($review) && $review->rating == 7 ? 'selected' : '' }}>7 - Good</option>
|
||||||
|
<option value="6" {{ isset($review) && $review->rating == 6 ? 'selected' : '' }}>6 - Fair</option>
|
||||||
|
<option value="5" {{ isset($review) && $review->rating == 5 ? 'selected' : '' }}>5 - Average</option>
|
||||||
|
<option value="4" {{ isset($review) && $review->rating == 4 ? 'selected' : '' }}>4 - Below Average</option>
|
||||||
|
<option value="3" {{ isset($review) && $review->rating == 3 ? 'selected' : '' }}>3 - Poor</option>
|
||||||
|
<option value="2" {{ isset($review) && $review->rating == 2 ? 'selected' : '' }}>2 - Bad</option>
|
||||||
|
<option value="1" {{ isset($review) && $review->rating == 1 ? 'selected' : '' }}>1 - Terrible</option>
|
||||||
|
</select>
|
||||||
|
@error('rating')
|
||||||
|
<div class="text-danger mt-1">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="form-label">Review</label>
|
||||||
|
<textarea name="content" id="content" class="form-control bg-dark text-white @error('content') is-invalid @enderror" rows="10" required>{{ old('content', $review->content ?? '') }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="text-danger mt-1">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" name="recommended" id="recommended" class="form-check-input" value="1" {{ isset($review) && $review->recommended ? 'checked' : '' }}>
|
||||||
|
<label for="recommended" class="form-check-label">I recommend this game</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
{{ isset($review) ? 'Update Review' : 'Submit Review' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@error('rating')
|
|
||||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="form-label">Recommendation</label>
|
|
||||||
<div class="recommendation-buttons d-flex gap-3">
|
|
||||||
<input type="radio" name="recommended" id="recommended1" value="1" class="recommendation-input"
|
|
||||||
{{ old('recommended', $review->recommended ?? '') == 1 ? 'checked' : '' }}>
|
|
||||||
<label for="recommended1" class="recommendation-button btn btn-outline-success">
|
|
||||||
<i class="fas fa-thumbs-up me-2"></i> Recommended
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input type="radio" name="recommended" id="recommended0" value="0" class="recommendation-input"
|
|
||||||
{{ old('recommended', $review->recommended ?? '') == 0 ? 'checked' : '' }}>
|
|
||||||
<label for="recommended0" class="recommendation-button btn btn-outline-danger">
|
|
||||||
<i class="fas fa-thumbs-down me-2"></i> Not Recommended
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
@error('recommended')
|
|
||||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-gradient">
|
|
||||||
<i class="fas fa-paper-plane me-2"></i> {{ isset($review) ? 'Update Review' : 'Post Review' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@push('styles')
|
@push('styles')
|
||||||
|
<link href="{{ asset('css/star-rating.css') }}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.rating-container {
|
.star-rating {
|
||||||
display: flex;
|
--gl-star-color: #fdd835;
|
||||||
flex-direction: column;
|
--gl-star-color-inactive: #dcdce6;
|
||||||
align-items: center;
|
--gl-star-size: 32px;
|
||||||
}
|
--gl-tooltip-border-radius: 4px;
|
||||||
|
--gl-tooltip-font-size: 1rem;
|
||||||
.rating-stars {
|
--gl-tooltip-font-weight: 400;
|
||||||
font-size: 1.5rem;
|
--gl-tooltip-line-height: 1;
|
||||||
position: relative;
|
--gl-tooltip-margin: 12px;
|
||||||
}
|
--gl-tooltip-padding: .5em 1em;
|
||||||
|
--gl-tooltip-background: rgba(17,17,17, .9);
|
||||||
.rating-item {
|
--gl-tooltip-color: #fff;
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-star {
|
|
||||||
color: #6c757d;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-number {
|
|
||||||
position: absolute;
|
|
||||||
top: -20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #6c757d;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-item:hover .rating-number {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-item:hover .rating-star,
|
|
||||||
.rating-item:hover ~ .rating-item .rating-star,
|
|
||||||
.rating-input:checked ~ .rating-item .rating-star {
|
|
||||||
color: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-input:checked + .rating-star {
|
|
||||||
color: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-value {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommendation-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommendation-button {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommendation-button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommendation-input:checked + .recommendation-button {
|
|
||||||
background-color: var(--bs-btn-bg);
|
|
||||||
color: var(--bs-btn-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#recommended1:checked + .recommendation-button {
|
|
||||||
background-color: var(--bs-success);
|
|
||||||
border-color: var(--bs-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
#recommended0:checked + .recommendation-button {
|
|
||||||
background-color: var(--bs-danger);
|
|
||||||
border-color: var(--bs-danger);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
|
<script src="{{ asset('js/star-rating.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
var stars = new StarRating('.star-rating', {
|
||||||
const ratingContainer = document.querySelector('.rating-stars');
|
classNames: {
|
||||||
const currentRating = document.querySelector('.current-rating');
|
active: "gl-active",
|
||||||
const ratingInputs = document.querySelectorAll('.rating-input');
|
base: "gl-star-rating",
|
||||||
|
selected: "gl-selected",
|
||||||
ratingContainer.addEventListener('mouseover', function(e) {
|
},
|
||||||
const ratingItem = e.target.closest('.rating-item');
|
clearable: true,
|
||||||
if (!ratingItem) return;
|
maxStars: 10,
|
||||||
|
tooltip: 'Select a Rating'
|
||||||
const rating = ratingItem.dataset.rating;
|
|
||||||
currentRating.textContent = rating;
|
|
||||||
|
|
||||||
// Highlight stars up to the hovered rating
|
|
||||||
const stars = document.querySelectorAll('.rating-star');
|
|
||||||
stars.forEach((star, index) => {
|
|
||||||
if (index < rating) {
|
|
||||||
star.style.color = '#ffc107';
|
|
||||||
} else {
|
|
||||||
star.style.color = '#6c757d';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ratingContainer.addEventListener('mouseout', function() {
|
|
||||||
const checkedInput = document.querySelector('.rating-input:checked');
|
|
||||||
if (checkedInput) {
|
|
||||||
currentRating.textContent = checkedInput.value;
|
|
||||||
// Restore the checked state colors
|
|
||||||
const stars = document.querySelectorAll('.rating-star');
|
|
||||||
stars.forEach((star, index) => {
|
|
||||||
if (index < checkedInput.value) {
|
|
||||||
star.style.color = '#ffc107';
|
|
||||||
} else {
|
|
||||||
star.style.color = '#6c757d';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
currentRating.textContent = '0';
|
|
||||||
// Reset all stars to default color
|
|
||||||
const stars = document.querySelectorAll('.rating-star');
|
|
||||||
stars.forEach(star => {
|
|
||||||
star.style.color = '#6c757d';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update current rating when a star is clicked
|
|
||||||
ratingInputs.forEach(input => {
|
|
||||||
input.addEventListener('change', function() {
|
|
||||||
currentRating.textContent = this.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $review->game->title . ' - Review by ' . $review->user->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="mb-0">{{ $review->game->title }}</h4>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="rating-stars d-flex gap-1 me-2">
|
||||||
|
@for($i = 1; $i <= 10; $i++)
|
||||||
|
<i class="fas fa-star {{ $i <= $review->rating ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
@if($review->recommended)
|
||||||
|
<span class="badge bg-success">Recommended</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">Not Recommended</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<a href="{{ route('profile.show', $review->user) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $review->user->name }}
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-clock"></i> {{ $review->created_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="review-content">
|
||||||
|
{!! nl2br(e($review->content)) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(auth()->id() === $review->user_id)
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<a href="{{ route('reviews.edit', $review) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Edit Review
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('reviews.destroy', $review) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete this review?')">
|
||||||
|
<i class="fas fa-trash"></i> Delete Review
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Game Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{{ $review->game->cover_url }}" alt="{{ $review->game->title }}" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted mb-2">Platforms</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($review->game->platforms as $platform)
|
||||||
|
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted mb-2">Genres</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($review->game->genres as $genre)
|
||||||
|
<span class="badge bg-primary">{{ $genre->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6 class="text-muted mb-2">Release Date</h6>
|
||||||
|
<p class="mb-0">{{ $review->game->first_release_date?->format('F j, Y') ?? 'TBA' }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('games.show', $review->game) }}" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-gamepad"></i> View Game
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
+16
-12
@@ -15,24 +15,28 @@ Route::get('/dashboard', function () {
|
|||||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
// Authenticated profile routes
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
|
||||||
|
Route::get('/profile/edit', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
|
Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
|
Route::get('/profile/reviews', [ProfileController::class, 'reviews'])->name('profile.reviews');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
|
// Review routes
|
||||||
|
Route::get('/games/{game:slug}/reviews/create', [ReviewController::class, 'create'])->name('games.reviews.create');
|
||||||
|
Route::post('/games/{game:slug}/reviews', [ReviewController::class, 'store'])->name('games.reviews.store');
|
||||||
|
Route::get('/reviews/{review}', [ReviewController::class, 'show'])->name('reviews.show');
|
||||||
|
Route::get('/reviews/{review}/edit', [ReviewController::class, 'edit'])->name('reviews.edit');
|
||||||
|
Route::put('/reviews/{review}', [ReviewController::class, 'update'])->name('reviews.update');
|
||||||
|
Route::delete('/reviews/{review}', [ReviewController::class, 'destroy'])->name('reviews.destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public profile routes
|
||||||
|
Route::get('/profile/{user:slug}', [ProfileController::class, 'show'])->name('profile.show');
|
||||||
|
|
||||||
Route::get('/games', [App\Http\Controllers\GameController::class, 'index'])->name('games.index');
|
Route::get('/games', [App\Http\Controllers\GameController::class, 'index'])->name('games.index');
|
||||||
Route::get('/games/{game:slug}', [App\Http\Controllers\GameController::class, 'show'])->name('games.show');
|
Route::get('/games/{game:slug}', [App\Http\Controllers\GameController::class, 'show'])->name('games.show');
|
||||||
|
|
||||||
// Review routes
|
|
||||||
Route::middleware('auth')->group(function () {
|
|
||||||
Route::get('/games/{game:slug}/reviews/create', [App\Http\Controllers\ReviewController::class, 'create'])->name('games.reviews.create');
|
|
||||||
Route::post('/games/{game:slug}/reviews', [App\Http\Controllers\ReviewController::class, 'store'])->name('games.reviews.store');
|
|
||||||
Route::get('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'show'])->name('games.reviews.show');
|
|
||||||
Route::get('/games/{game:slug}/reviews/{review}/edit', [App\Http\Controllers\ReviewController::class, 'edit'])->name('games.reviews.edit');
|
|
||||||
Route::put('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'update'])->name('games.reviews.update');
|
|
||||||
Route::delete('/games/{game:slug}/reviews/{review}', [App\Http\Controllers\ReviewController::class, 'destroy'])->name('games.reviews.destroy');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Public review routes
|
// Public review routes
|
||||||
Route::get('/games/{game:slug}/reviews', [App\Http\Controllers\ReviewController::class, 'index'])->name('games.reviews.index');
|
Route::get('/games/{game:slug}/reviews', [App\Http\Controllers\ReviewController::class, 'index'])->name('games.reviews.index');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user