feat: ship creator journey v2 and profile updates
This commit is contained in:
215
app/Http/Controllers/Settings/FeaturedArtworkAdminController.php
Normal file
215
app/Http/Controllers/Settings/FeaturedArtworkAdminController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user