mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge pull request #65 from waffle-lord/impl/mod-card-moderation-options
Impl/mod moderation options
This commit is contained in:
commit
36222169fb
103
app/Livewire/Mod/ModerationActionButton.php
Normal file
103
app/Livewire/Mod/ModerationActionButton.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Mod;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ModerationActionButton extends Component
|
||||
{
|
||||
public ?string $moderatedObjectId = null;
|
||||
|
||||
public string $guid = '';
|
||||
|
||||
public string $actionType;
|
||||
|
||||
public string $targetType = '';
|
||||
|
||||
public bool $allowActions = false;
|
||||
|
||||
public bool $isRunning = false;
|
||||
|
||||
protected $listeners = ['refreshComponent' => '$refresh'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->guid = uniqid('', true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$this->allowActions = ! $this->isRunning;
|
||||
|
||||
return view('livewire.mod.moderation-action-button');
|
||||
}
|
||||
|
||||
public function runActionEvent(): void
|
||||
{
|
||||
$this->isRunning = true;
|
||||
$this->dispatch("startAction.{$this->guid}");
|
||||
}
|
||||
|
||||
#[On('startAction.{guid}')]
|
||||
public function invokeAction(): void
|
||||
{
|
||||
if ($this->moderatedObjectId == null || $this->moderatedObjectId == '') {
|
||||
Log::info('Failed: no ID specified.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info("Object ID: $this->moderatedObjectId");
|
||||
|
||||
if ($this->targetType !== 'mod' && $this->targetType !== 'modVersion') {
|
||||
Log::info('Failed: invalid target type.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->targetType) {
|
||||
case 'mod':
|
||||
$moderatedObject = Mod::where('id', '=', $this->moderatedObjectId)->first();
|
||||
break;
|
||||
|
||||
case 'modVersion':
|
||||
$moderatedObject = ModVersion::where('id', '=', $this->moderatedObjectId)->first();
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Failed: invalid target type.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($moderatedObject == null) {
|
||||
Log::info('Failed: moderated object is null');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->actionType) {
|
||||
case 'delete':
|
||||
|
||||
$moderatedObject->delete();
|
||||
break;
|
||||
|
||||
case 'enable':
|
||||
case 'disable':
|
||||
|
||||
$moderatedObject->toggleDisabled();
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Failed: invalid action type.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->js('window.location.reload()');
|
||||
}
|
||||
}
|
35
app/Livewire/Mod/ModerationOptions.php
Normal file
35
app/Livewire/Mod/ModerationOptions.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Mod;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ModerationOptions extends Component
|
||||
{
|
||||
public string $objectId;
|
||||
|
||||
public string $targetType;
|
||||
|
||||
public bool $disabled;
|
||||
|
||||
public string $displayName;
|
||||
|
||||
public bool $showDeleteDialog = false;
|
||||
|
||||
public bool $showDisableDialog = false;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.mod.moderation-options');
|
||||
}
|
||||
|
||||
public function confirmDelete(): void
|
||||
{
|
||||
$this->showDeleteDialog = true;
|
||||
}
|
||||
|
||||
public function confirmDisable(): void
|
||||
{
|
||||
$this->showDisableDialog = true;
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Http\Filters\V1\QueryFilter;
|
||||
use App\Models\Scopes\DisabledScope;
|
||||
use App\Models\Scopes\PublishedScope;
|
||||
use App\Traits\CanModerate;
|
||||
use Database\Factories\ModFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -54,6 +54,7 @@ use Override;
|
||||
*/
|
||||
class Mod extends Model
|
||||
{
|
||||
use CanModerate;
|
||||
/** @use HasFactory<ModFactory> */
|
||||
use HasFactory;
|
||||
|
||||
@ -66,7 +67,6 @@ class Mod extends Model
|
||||
#[Override]
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Models\Scopes\DisabledScope;
|
||||
use App\Models\Scopes\PublishedScope;
|
||||
use App\Support\Version;
|
||||
use App\Traits\CanModerate;
|
||||
use Database\Factories\ModVersionFactory;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -50,6 +50,7 @@ use Override;
|
||||
*/
|
||||
class ModVersion extends Model
|
||||
{
|
||||
use CanModerate;
|
||||
/** @use HasFactory<ModVersionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
@ -68,8 +69,6 @@ class ModVersion extends Model
|
||||
#[Override]
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
|
||||
static::saving(function (ModVersion $modVersion): void {
|
||||
|
@ -192,6 +192,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return Str::lower($this->role?->name) === 'administrator';
|
||||
}
|
||||
|
||||
/**
|
||||
* Conveniently check is a user is a mod or an admin
|
||||
*/
|
||||
public function isModOrAdmin(): bool
|
||||
{
|
||||
return $this->isMod() || $this->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to instead use the queued version of the VerifyEmail notification.
|
||||
*/
|
||||
|
@ -22,7 +22,11 @@ class ModPolicy
|
||||
*/
|
||||
public function view(?User $user, Mod $mod): bool
|
||||
{
|
||||
// Everyone can view mods.
|
||||
// Everyone can view mods, unless they are disabled.
|
||||
if ($mod->disabled && ! $user?->isModOrAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -39,7 +43,7 @@ class ModPolicy
|
||||
*/
|
||||
public function update(User $user, Mod $mod): bool
|
||||
{
|
||||
return false;
|
||||
return $user->isMod() || $user->isAdmin() || $mod->users->contains($user);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,7 +51,10 @@ class ModPolicy
|
||||
*/
|
||||
public function delete(User $user, Mod $mod): bool
|
||||
{
|
||||
return false;
|
||||
// I'm guessing we want the mod author to also be able to do this?
|
||||
// what if there are multiple authors?
|
||||
// I'm leaving that out for now -waffle.lazy
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
12
app/Traits/CanModerate.php
Normal file
12
app/Traits/CanModerate.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
trait CanModerate
|
||||
{
|
||||
public function toggleDisabled(): void
|
||||
{
|
||||
$this->disabled = ! $this->disabled;
|
||||
$this->save();
|
||||
}
|
||||
}
|
@ -120,13 +120,14 @@ main a:not(.mod-list-component):not(.tab):not([role="menuitem"]) {
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
@layer components {
|
||||
.ribbon {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
@apply text-white bg-cyan-500 dark:bg-cyan-700;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
.ribbon {
|
||||
--f: .5em;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -138,9 +139,15 @@ main a:not(.mod-list-component):not(.tab):not([role="menuitem"]) {
|
||||
clip-path: polygon(100% calc(100% - var(--f)), 100% 100%, calc(100% - var(--f)) calc(100% - var(--f)), var(--f) calc(100% - var(--f)), 0 100%, 0 calc(100% - var(--f)), 999px calc(100% - var(--f) - 999px), calc(100% - 999px) calc(100% - var(--f) - 999px));
|
||||
transform: translate(calc((cos(45deg) - 1) * 100%), -100%) rotate(-45deg);
|
||||
transform-origin: 100% 100%;
|
||||
background-color: #0e7490;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon-red {
|
||||
@apply ribbon;
|
||||
@apply text-white bg-red-500 dark:bg-red-700;
|
||||
}
|
||||
|
||||
|
||||
.rainbow {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -1,12 +1,15 @@
|
||||
@props(['mod', 'version'])
|
||||
|
||||
<a href="{{ $mod->detailUrl() }}" class="mod-list-component relative mx-auto w-full max-w-2xl">
|
||||
@if ($mod->featured && !request()->routeIs('home'))
|
||||
@if ($mod->featured && !$mod->disabled && !request()->routeIs('home'))
|
||||
<div class="ribbon z-10">{{ __('Featured!') }}</div>
|
||||
@endif
|
||||
@if ($mod->disabled)
|
||||
<div class="ribbon-red z-10">{{ __('Disabled') }}</div>
|
||||
@endif
|
||||
<div class="flex flex-col group h-full w-full bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl overflow-hidden hover:shadow-lg hover:bg-gray-50 dark:hover:bg-black hover:shadow-gray-400 dark:hover:shadow-black transition-colors ease-out duration-700">
|
||||
<div class="h-auto md:h-full md:flex">
|
||||
<div class="h-auto md:h-full md:shrink-0 overflow-hidden">
|
||||
<div class="relative h-auto md:h-full md:shrink-0 overflow-hidden">
|
||||
@if ($mod->thumbnail)
|
||||
<img src="{{ $mod->thumbnailUrl }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||
@else
|
||||
|
@ -173,7 +173,9 @@
|
||||
@if ($mods->isNotEmpty())
|
||||
<div class="my-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
@foreach ($mods as $mod)
|
||||
@if(!$mod->disabled || (auth()->check() && auth()->user()->isModOrAdmin()))
|
||||
<x-mod-card :mod="$mod" :version="$mod->latestVersion" />
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
|
@ -0,0 +1,18 @@
|
||||
<div class="flex gap-4">
|
||||
@if($this->allowActions)
|
||||
<x-button x-on:click="show = false">
|
||||
{{ __('Cancel') }}
|
||||
</x-button>
|
||||
<x-danger-button wire:click="runActionEvent">
|
||||
{{ __(ucfirst($this->actionType)) }}
|
||||
</x-danger-button>
|
||||
@endif
|
||||
@if($this->isRunning)
|
||||
<div class="text-blue-600">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>.spinner_ajPY{transform-origin:center;animation:spinner_AtaB .75s infinite linear}@keyframes spinner_AtaB{100%{transform:rotate(360deg)}}</style>
|
||||
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" class="spinner_ajPY"/>
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
79
resources/views/livewire/mod/moderation-options.blade.php
Normal file
79
resources/views/livewire/mod/moderation-options.blade.php
Normal file
@ -0,0 +1,79 @@
|
||||
<div>
|
||||
<x-dropdown alignment="right" contentClasses="py-1 rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<x-slot name="trigger">
|
||||
<button class="relative text-blue-400 dark:text-blue-500 hover:text-blue-600 dark:hover:text-blue-700">
|
||||
{{-- Icon (shield with keyhole)--}}
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 5.75V11c0 5.001 2.958 8.676 8.725 10.948a.75.75 0 0 0 .55 0C18.042 19.676 21 16 21 11V5.75a.75.75 0 0 0-.75-.75c-2.663 0-5.258-.943-7.8-2.85a.75.75 0 0 0-.9 0C9.008 4.057 6.413 5 3.75 5a.75.75 0 0 0-.75.75ZM13.995 11a2 2 0 0 1-1.245 1.852v2.398a.75.75 0 0 1-1.5 0v-2.394A2 2 0 1 1 13.995 11Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div>
|
||||
<button wire:click.prevent="confirmDisable" class="p-2 h-full w-full text-blue-500 dark:text-blue-500 bg-gray-200 dark:bg-gray-800 hover:text-blue-400 dark:hover:text-blue-400">
|
||||
<div class="flex">
|
||||
<span class="pr-2">
|
||||
@if ($this->disabled)
|
||||
{{-- Icon (circle with checkmark)--}}
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z" />
|
||||
</svg>
|
||||
@else
|
||||
{{-- Icon (circle with dash)--}}
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2.001c5.524 0 10 4.477 10 10s-4.476 10-10 10c-5.522 0-10-4.477-10-10s4.478-10 10-10Zm4.25 9.25h-8.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5Z" />
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
{{$this->disabled ? __('Enable') : __('Disable') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<div>
|
||||
<button wire:click.prevent="confirmDelete" class="p-2 h-full w-full text-red-500 dark:text-red-500 bg-gray-200 dark:bg-gray-800 hover:text-red-400 dark:hover:text-red-400">
|
||||
<div class="flex">
|
||||
<span class="pr-2">
|
||||
{{-- Icon (trash can)--}}
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 6a1 1 0 0 1-.883.993L20.5 7h-.845l-1.231 12.52A2.75 2.75 0 0 1 15.687 22H8.313a2.75 2.75 0 0 1-2.737-2.48L4.345 7H3.5a1 1 0 0 1 0-2h5a3.5 3.5 0 1 1 7 0h5a1 1 0 0 1 1 1Zm-7.25 3.25a.75.75 0 0 0-.743.648L13.5 10v7l.007.102a.75.75 0 0 0 1.486 0L15 17v-7l-.007-.102a.75.75 0 0 0-.743-.648Zm-4.5 0a.75.75 0 0 0-.743.648L9 10v7l.007.102a.75.75 0 0 0 1.486 0L10.5 17v-7l-.007-.102a.75.75 0 0 0-.743-.648ZM12 3.5A1.5 1.5 0 0 0 10.5 5h3A1.5 1.5 0 0 0 12 3.5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ __('Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
</x-dropdown>
|
||||
|
||||
@push('modals')
|
||||
<x-dialog-modal wire:model="showDisableDialog">
|
||||
<x-slot name="title">
|
||||
<h2 class="text-2xl">{{__('Confirm')}} {{__($this->disabled ? 'Enable' : 'Disable')}}</h2>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<p>Are you sure you want to {{__($this->disabled ? 'enable' : 'disable')}} '{{$this->displayName}}'?</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<livewire:mod.moderation-action-button actionType="{{ $this->disabled ? 'enable' : 'disable' }}" :targetType="$targetType" :moderatedObjectId="$objectId" />
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
@endpush
|
||||
|
||||
@push('modals')
|
||||
<x-dialog-modal wire:model="showDeleteDialog">
|
||||
<x-slot name="title">
|
||||
<h2 class="text-2xl">{{ __('Confirm') }} {{ __('Delete') }}</h2>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<p>Are you sure you want to {{__('delete')}} '{{$this->displayName}}'?</p>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<livewire:mod.moderation-action-button actionType='delete' :targetType="$targetType" :moderatedObjectId="$objectId" />
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
@endpush
|
||||
</div>
|
@ -11,9 +11,17 @@
|
||||
|
||||
{{-- Main Mod Details Card --}}
|
||||
<div class="relative p-4 sm:p-6 text-center sm:text-left bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||
@if ($mod->featured)
|
||||
@if (auth()->check() && auth()->user()->isModOrAdmin())
|
||||
<div class="absolute top-0 right-0 z-50 m-2">
|
||||
<livewire:mod.moderation-options :objectId="$mod->id" targetType="mod" :displayName="$mod->name" :disabled="$mod->disabled" />
|
||||
</div>
|
||||
@endif
|
||||
@if ($mod->featured && !$mod->disabled)
|
||||
<div class="ribbon z-10">{{ __('Featured!') }}</div>
|
||||
@endif
|
||||
@if ($mod->disabled)
|
||||
<div class="ribbon-red z-10">{{ __('Disabled') }}</div>
|
||||
@endif
|
||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<div class="grow-0 shrink-0 flex justify-center items-center">
|
||||
@if ($mod->thumbnail)
|
||||
@ -98,7 +106,15 @@
|
||||
<div x-show="selectedTab === 'versions'">
|
||||
@foreach ($mod->versions as $version)
|
||||
<div class="p-4 mb-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||
@if($version->disabled)
|
||||
<div class="ribbon-red z-10">{{ __('Disabled') }}</div>
|
||||
@endif
|
||||
<div class="pb-6 border-b-2 border-gray-200 dark:border-gray-800">
|
||||
@if (auth()->check() && auth()->user()->isModOrAdmin())
|
||||
<div class="absolute top-0 right-0 z-50 m-2">
|
||||
<livewire:mod.moderation-options :objectId="$version->id" targetType="modVersion" :displayName="$version->version" :disabled="$version->disabled" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center justify-between">
|
||||
<a class="text-2xl font-extrabold text-gray-900 dark:text-white" href="{{ $version->downloadUrl() }}">
|
||||
{{ __('Version') }} {{ $version->version }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user