215 lines
7.6 KiB
PHP
215 lines
7.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Settings;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ArtworkFeature;
|
|
use App\Services\FeaturedArtworkAdminService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class FeaturedArtworkAdminController extends Controller
|
|
{
|
|
public function __construct(private readonly FeaturedArtworkAdminService $featuredArtworks)
|
|
{
|
|
}
|
|
|
|
public function index(): Response
|
|
{
|
|
return Inertia::render('Collection/FeaturedArtworksAdmin', array_merge(
|
|
$this->featuredArtworks->pageProps(),
|
|
[
|
|
'endpoints' => [
|
|
'search' => route('admin.cp.artworks.featured.search'),
|
|
'store' => route('admin.cp.artworks.featured.store'),
|
|
'updatePattern' => route('admin.cp.artworks.featured.update', ['feature' => '__FEATURE__']),
|
|
'togglePattern' => route('admin.cp.artworks.featured.toggle', ['feature' => '__FEATURE__']),
|
|
'forceHeroPattern' => route('admin.cp.artworks.featured.force-hero', ['feature' => '__FEATURE__']),
|
|
'destroyPattern' => route('admin.cp.artworks.featured.delete', ['feature' => '__FEATURE__']),
|
|
],
|
|
'capabilities' => [
|
|
'forceHeroEnabled' => $this->hasForceHeroColumn(),
|
|
],
|
|
'seo' => [
|
|
'title' => 'Featured Artworks — Skinbase Nova',
|
|
'description' => 'Editorial controls for homepage featured artworks and the current hero winner.',
|
|
'canonical' => route('admin.cp.artworks.featured.main'),
|
|
'robots' => 'noindex,follow',
|
|
],
|
|
],
|
|
))->rootView('collections');
|
|
}
|
|
|
|
public function search(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'q' => ['required', 'string', 'min:1', 'max:120'],
|
|
]);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'results' => $this->featuredArtworks->searchArtworks((string) $validated['q']),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $this->validateStore($request);
|
|
$actor = $this->currentActor($request);
|
|
|
|
ArtworkFeature::query()->create([
|
|
'artwork_id' => (int) $validated['artwork_id'],
|
|
'priority' => (int) $validated['priority'],
|
|
'featured_at' => Carbon::parse((string) $validated['featured_at']),
|
|
'expires_at' => filled($validated['expires_at'] ?? null) ? Carbon::parse((string) $validated['expires_at']) : null,
|
|
'is_active' => (bool) $validated['is_active'],
|
|
'created_by' => (int) $actor->id,
|
|
]);
|
|
|
|
return $this->mutationResponse('Featured artwork added.');
|
|
}
|
|
|
|
public function update(Request $request, ArtworkFeature $feature): JsonResponse
|
|
{
|
|
$validated = $this->validateUpdate($request);
|
|
$this->ensureStateAvailable($feature, (bool) $validated['is_active']);
|
|
|
|
$feature->fill([
|
|
'priority' => (int) $validated['priority'],
|
|
'featured_at' => Carbon::parse((string) $validated['featured_at']),
|
|
'expires_at' => filled($validated['expires_at'] ?? null) ? Carbon::parse((string) $validated['expires_at']) : null,
|
|
'is_active' => (bool) $validated['is_active'],
|
|
]);
|
|
$feature->save();
|
|
|
|
return $this->mutationResponse('Featured artwork updated.');
|
|
}
|
|
|
|
public function toggle(ArtworkFeature $feature): JsonResponse
|
|
{
|
|
$nextState = ! (bool) $feature->is_active;
|
|
$this->ensureStateAvailable($feature, $nextState);
|
|
|
|
$feature->forceFill([
|
|
'is_active' => $nextState,
|
|
])->save();
|
|
|
|
return $this->mutationResponse($nextState ? 'Featured artwork activated.' : 'Featured artwork deactivated.');
|
|
}
|
|
|
|
public function toggleForceHero(ArtworkFeature $feature): JsonResponse
|
|
{
|
|
$this->ensureForceHeroAvailable();
|
|
|
|
$nextState = ! (bool) $feature->force_hero;
|
|
|
|
DB::transaction(function () use ($feature, $nextState): void {
|
|
if ($nextState) {
|
|
ArtworkFeature::query()
|
|
->where('force_hero', true)
|
|
->whereNull('deleted_at')
|
|
->whereKeyNot($feature->id)
|
|
->update(['force_hero' => false]);
|
|
}
|
|
|
|
$feature->forceFill([
|
|
'force_hero' => $nextState,
|
|
])->save();
|
|
});
|
|
|
|
return $this->mutationResponse($nextState ? 'Force hero enabled.' : 'Force hero disabled.');
|
|
}
|
|
|
|
public function destroy(ArtworkFeature $feature): JsonResponse
|
|
{
|
|
$feature->delete();
|
|
|
|
return $this->mutationResponse('Featured artwork entry deleted.');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function validateStore(Request $request): array
|
|
{
|
|
return $request->validate([
|
|
'artwork_id' => [
|
|
'required',
|
|
'integer',
|
|
Rule::exists('artworks', 'id'),
|
|
Rule::unique('artwork_features', 'artwork_id')->where(fn ($query) => $query->whereNull('deleted_at')),
|
|
],
|
|
'priority' => ['required', 'integer', 'min:0', 'max:65535'],
|
|
'featured_at' => ['required', 'date'],
|
|
'expires_at' => ['nullable', 'date', 'after:featured_at'],
|
|
'is_active' => ['required', 'boolean'],
|
|
], [
|
|
'artwork_id.unique' => 'This artwork already has a featured entry. Edit the existing row instead.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function validateUpdate(Request $request): array
|
|
{
|
|
return $request->validate([
|
|
'priority' => ['required', 'integer', 'min:0', 'max:65535'],
|
|
'featured_at' => ['required', 'date'],
|
|
'expires_at' => ['nullable', 'date', 'after:featured_at'],
|
|
'is_active' => ['required', 'boolean'],
|
|
]);
|
|
}
|
|
|
|
private function ensureStateAvailable(ArtworkFeature $feature, bool $isActive): void
|
|
{
|
|
$conflictExists = ArtworkFeature::query()
|
|
->where('artwork_id', $feature->artwork_id)
|
|
->where('is_active', $isActive)
|
|
->whereNull('deleted_at')
|
|
->whereKeyNot($feature->id)
|
|
->exists();
|
|
|
|
if ($conflictExists) {
|
|
throw ValidationException::withMessages([
|
|
'is_active' => 'Another featured entry for this artwork already uses that active state.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function mutationResponse(string $message): JsonResponse
|
|
{
|
|
return response()->json(array_merge([
|
|
'ok' => true,
|
|
'message' => $message,
|
|
], $this->featuredArtworks->pageProps()));
|
|
}
|
|
|
|
private function currentActor(Request $request): object
|
|
{
|
|
return $request->user('controlpanel') ?? $request->user() ?? abort(403, 'Admin access required.');
|
|
}
|
|
|
|
private function ensureForceHeroAvailable(): void
|
|
{
|
|
if (! $this->hasForceHeroColumn()) {
|
|
throw ValidationException::withMessages([
|
|
'force_hero' => 'Run php artisan migrate to enable force hero controls.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function hasForceHeroColumn(): bool
|
|
{
|
|
return Schema::hasColumn('artwork_features', 'force_hero');
|
|
}
|
|
} |