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 Nova', 'description' => 'Editorial controls for homepage featured artworks and the current hero winner.', 'canonical' => route($routePrefix . 'main'), 'robots' => 'noindex,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 */ 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 */ 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'); } }