Files
SkinbaseNova/app/Http/Controllers/Settings/FeaturedArtworkAdminController.php

218 lines
7.8 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(Request $request): Response
{
$isAdminSurface = $request->routeIs('admin.artworks.featured.*');
$routePrefix = $isAdminSurface ? 'admin.artworks.featured.' : 'admin.cp.artworks.featured.';
return Inertia::render($isAdminSurface ? 'Admin/FeaturedArtworks' : 'Collection/FeaturedArtworksAdmin', array_merge(
$this->featuredArtworks->pageProps(),
[
'endpoints' => [
'search' => route($routePrefix . 'search'),
'store' => route($routePrefix . 'store'),
'updatePattern' => route($routePrefix . 'update', ['feature' => '__FEATURE__']),
'togglePattern' => route($routePrefix . 'toggle', ['feature' => '__FEATURE__']),
'forceHeroPattern' => route($routePrefix . 'force-hero', ['feature' => '__FEATURE__']),
'destroyPattern' => route($routePrefix . 'delete', ['feature' => '__FEATURE__']),
],
'capabilities' => [
'forceHeroEnabled' => $this->hasForceHeroColumn(),
],
'seo' => [
'title' => 'Featured Artworks — Skinbase',
'description' => 'Editorial controls for homepage featured artworks and the current hero winner.',
'canonical' => route($routePrefix . 'main'),
'robots' => 'index,follow',
],
],
))->rootView($isAdminSurface ? 'admin' : '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');
}
}