authorize('create', Collection::class); $initialMode = $request->query('mode') === Collection::MODE_SMART ? Collection::MODE_SMART : Collection::MODE_MANUAL; return Inertia::render('Collection/CollectionManage', [ 'mode' => 'create', 'collection' => null, 'layoutModules' => $this->collections->getLayoutModuleDefinitions(), 'attachedArtworks' => [], 'availableArtworks' => [], 'smartPreview' => null, 'smartRuleOptions' => $this->collections->getSmartRuleOptions($request->user()), 'initialMode' => $initialMode, 'featuredLimit' => (int) config('collections.featured_limit', 3), 'owner' => $this->ownerPayload($request), 'members' => [], 'submissions' => [], 'comments' => [], 'duplicateCandidates' => [], 'canonicalTarget' => null, 'inviteExpiryDays' => (int) config('collections.invites.expires_after_days', 7), 'endpoints' => [ 'store' => route('settings.collections.store'), 'smartPreview' => route('settings.collections.smart.preview'), 'profileCollections' => route('profile.tab', [ 'username' => strtolower((string) $request->user()->username), 'tab' => 'collections', ]), ], ])->rootView('collections'); } public function show(Request $request, Collection $collection) { $this->authorize('manageArtworks', $collection); $collection->loadMissing(['user.profile', 'coverArtwork']); return Inertia::render('Collection/CollectionManage', [ 'mode' => 'edit', 'collection' => $this->collections->mapCollectionDetailPayload($collection, true), 'layoutModules' => $this->collections->mapCollectionDetailPayload($collection, true)['layout_modules'], 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'availableArtworks' => $this->collections->getAvailableArtworkOptions($collection, $request->user()), 'smartPreview' => $collection->isSmart() && is_array($collection->smart_rules_json) ? $this->collections->previewSmartCollection($request->user(), $collection->smart_rules_json) : null, 'smartRuleOptions' => $this->collections->getSmartRuleOptions($request->user()), 'initialMode' => $collection->mode, 'featuredLimit' => (int) config('collections.featured_limit', 3), 'owner' => $this->ownerPayload($request), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), 'duplicateCandidates' => $this->merge->reviewCandidates($collection->loadMissing('user'), true), 'canonicalTarget' => $collection->canonical_collection_id ? $this->collections->mapCollectionCardPayloads([ Collection::query()->findOrFail((int) $collection->canonical_collection_id)->loadMissing('user'), ], true)[0] : null, 'linkedCollections' => $this->collections->mapCollectionCardPayloads($this->linkedCollections->linkedCollections($collection), true), 'linkedCollectionOptions' => $this->collections->mapCollectionCardPayloads($this->linkedCollections->manageableLinkOptions($collection, $request->user()), true), 'entityLinks' => $this->entityLinks->links($collection, false), 'entityLinkOptions' => $this->entityLinks->manageableOptions($collection), 'inviteExpiryDays' => (int) config('collections.invites.expires_after_days', 7), 'endpoints' => [ 'update' => route('settings.collections.update', ['collection' => $collection->id]), 'updatePresentation' => route('settings.collections.presentation', ['collection' => $collection->id]), 'updateCampaign' => route('settings.collections.campaign', ['collection' => $collection->id]), 'updateSeries' => route('settings.collections.series', ['collection' => $collection->id]), 'updateLifecycle' => route('settings.collections.lifecycle', ['collection' => $collection->id]), 'syncLinkedCollections' => route('settings.collections.linked.sync', ['collection' => $collection->id]), 'syncEntityLinks' => route('settings.collections.entity-links.sync', ['collection' => $collection->id]), 'delete' => route('settings.collections.destroy', ['collection' => $collection->id]), 'attach' => route('settings.collections.artworks.attach', ['collection' => $collection->id]), 'reorder' => route('settings.collections.artworks.reorder', ['collection' => $collection->id]), 'available' => route('settings.collections.artworks.available', ['collection' => $collection->id]), 'removePattern' => route('settings.collections.artworks.remove', ['collection' => $collection->id, 'artwork' => '__ARTWORK__']), 'edit' => route('settings.collections.edit', ['collection' => $collection->id]), 'feature' => route('settings.collections.feature', ['collection' => $collection->id]), 'unfeature' => route('settings.collections.unfeature', ['collection' => $collection->id]), 'smartPreview' => route('settings.collections.smart.preview'), 'aiSuggestTitle' => route('settings.collections.ai.suggest-title', ['collection' => $collection->id]), 'aiSuggestSummary' => route('settings.collections.ai.suggest-summary', ['collection' => $collection->id]), 'aiSuggestCover' => route('settings.collections.ai.suggest-cover', ['collection' => $collection->id]), 'aiSuggestGrouping' => route('settings.collections.ai.suggest-grouping', ['collection' => $collection->id]), 'aiSuggestRelatedArtworks' => route('settings.collections.ai.suggest-related-artworks', ['collection' => $collection->id]), 'aiSuggestTags' => route('settings.collections.ai.suggest-tags', ['collection' => $collection->id]), 'aiSuggestSeoDescription' => route('settings.collections.ai.suggest-seo-description', ['collection' => $collection->id]), 'aiExplainSmartRules' => route('settings.collections.ai.explain-smart-rules', ['collection' => $collection->id]), 'aiSuggestSplitThemes' => route('settings.collections.ai.suggest-split-themes', ['collection' => $collection->id]), 'aiSuggestMergeIdea' => route('settings.collections.ai.suggest-merge-idea', ['collection' => $collection->id]), 'aiQualityReview' => route('settings.collections.ai.quality-review', ['collection' => $collection->id]), 'updateSmartRules' => route('settings.collections.smart.rules', ['collection' => $collection->id]), 'inviteMember' => route('settings.collections.members.store', ['collection' => $collection->id]), 'memberUpdatePattern' => route('settings.collections.members.update', ['collection' => $collection->id, 'member' => '__MEMBER__']), 'memberTransferPattern' => route('settings.collections.members.transfer', ['collection' => $collection->id, 'member' => '__MEMBER__']), 'memberDeletePattern' => route('settings.collections.members.destroy', ['collection' => $collection->id, 'member' => '__MEMBER__']), 'acceptMemberPattern' => route('settings.collections.members.accept', ['member' => '__MEMBER__']), 'declineMemberPattern' => route('settings.collections.members.decline', ['member' => '__MEMBER__']), 'adminModerationUpdate' => $this->isAdmin($request) ? route('api.admin.collections.moderation.update', ['collection' => $collection->id]) : null, 'adminInteractionsUpdate' => $this->isAdmin($request) ? route('api.admin.collections.interactions.update', ['collection' => $collection->id]) : null, 'adminUnfeature' => $this->isAdmin($request) ? route('api.admin.collections.unfeature', ['collection' => $collection->id]) : null, 'adminMemberRemovePattern' => $this->isAdmin($request) ? route('api.admin.collections.members.destroy', ['collection' => $collection->id, 'member' => '__MEMBER__']) : null, 'submissionStore' => route('collections.submissions.store', ['collection' => $collection->id]), 'submissionApprovePattern' => route('collections.submissions.approve', ['submission' => '__SUBMISSION__']), 'submissionRejectPattern' => route('collections.submissions.reject', ['submission' => '__SUBMISSION__']), 'submissionDeletePattern' => route('collections.submissions.destroy', ['submission' => '__SUBMISSION__']), 'commentsIndex' => route('collections.comments.index', ['collection' => $collection->id]), 'commentsStore' => route('collections.comments.store', ['collection' => $collection->id]), 'commentDeletePattern' => route('collections.comments.destroy', ['collection' => $collection->id, 'comment' => '__COMMENT__']), 'public' => route('profile.collections.show', [ 'username' => strtolower((string) $collection->user->username), 'slug' => $collection->slug, ]), 'dashboard' => route('settings.collections.dashboard'), 'analytics' => route('settings.collections.analytics', ['collection' => $collection->id]), 'history' => route('settings.collections.history', ['collection' => $collection->id]), 'canonicalize' => route('settings.collections.canonicalize', ['collection' => $collection->id]), 'merge' => route('settings.collections.merge', ['collection' => $collection->id]), 'rejectDuplicate' => route('settings.collections.merge.reject', ['collection' => $collection->id]), 'staffSurfaces' => $this->isAdmin($request) ? route('settings.collections.surfaces.index') : null, 'staffProgramming' => $this->isAdmin($request) ? route('staff.collections.programming') : null, 'profileCollections' => route('profile.tab', [ 'username' => strtolower((string) $collection->user->username), 'tab' => 'collections', ]), ], 'viewer' => [ 'is_admin' => $this->isAdmin($request), ], ])->rootView('collections'); } public function edit(Request $request, Collection $collection) { return $this->show($request, $collection); } public function store(StoreCollectionRequest $request): RedirectResponse|JsonResponse { $this->authorize('create', Collection::class); $collection = $this->collections->createCollection($request->user(), $request->validated()); $redirectTo = (string) route('settings.collections.show', ['collection' => $collection->id]); return $this->jsonOrRedirect($request, [ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), 'redirect' => $redirectTo, ], $redirectTo); } public function update(UpdateCollectionRequest $request, Collection $collection): RedirectResponse|JsonResponse { $this->authorize('update', $collection); $collection = $this->collections->updateCollection($collection->loadMissing('user'), $request->validated(), $request->user()); $redirectTo = (string) route('settings.collections.show', ['collection' => $collection->id]); return $this->jsonOrRedirect($request, [ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), 'redirect' => $redirectTo, ], $redirectTo); } public function updatePresentation(UpdateCollectionPresentationRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); return $this->updateScopedSettings($request, $collection, $request->validated()); } public function updateCampaign(UpdateCollectionCampaignRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->campaigns->updateCampaign($collection, $request->validated(), $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'campaign' => $this->campaigns->campaignSummary($collection), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), ]); } public function updateSeries(UpdateCollectionSeriesRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->series->updateSeries($collection, $request->validated(), $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'series' => $this->series->summary($collection), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), ]); } public function updateLifecycle(UpdateCollectionLifecycleRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); return $this->updateScopedSettings($request, $collection, $request->validated()); } public function syncLinkedCollections(UpdateCollectionLinkedCollectionsRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->linkedCollections->syncLinks($collection->loadMissing('user'), $request->user(), $request->validated('related_collection_ids', [])); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'linkedCollections' => $this->collections->mapCollectionCardPayloads($this->linkedCollections->linkedCollections($collection), true), 'linkedCollectionOptions' => $this->collections->mapCollectionCardPayloads($this->linkedCollections->manageableLinkOptions($collection, $request->user()), true), ]); } public function syncEntityLinks(UpdateCollectionEntityLinksRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->entityLinks->syncLinks($collection->loadMissing('user'), $request->user(), $request->validated('entity_links', [])); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'entityLinks' => $this->entityLinks->links($collection, false), 'entityLinkOptions' => $this->entityLinks->manageableOptions($collection), ]); } public function destroy(Request $request, Collection $collection): RedirectResponse|JsonResponse { $this->authorize('delete', $collection); $profileCollectionsUrl = (string) route('profile.tab', [ 'username' => strtolower((string) $collection->user->username), 'tab' => 'collections', ]); $this->collections->deleteCollection($collection); return $this->jsonOrRedirect($request, [ 'ok' => true, 'redirect' => $profileCollectionsUrl, ], $profileCollectionsUrl); } public function attachArtworks(AttachCollectionArtworksRequest $request, Collection $collection): JsonResponse { $this->authorize('manageArtworks', $collection); $collection = $this->collections->attachArtworks($collection->loadMissing('user'), $request->user(), $request->validated('artwork_ids')); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'availableArtworks' => $this->collections->getAvailableArtworkOptions($collection, $request->user()), ]); } public function artworkCollectionOptions(Request $request): JsonResponse { $artwork = $this->resolveArtworkFromRequest($request, 4); abort_unless((int) $artwork->user_id === (int) $request->user()->id, 404); return response()->json([ 'data' => $this->collections->getCollectionOptionsForArtwork($request->user(), $artwork), 'meta' => [ 'create_url' => route('settings.collections.create'), 'artwork_id' => (int) $artwork->id, ], ]); } public function removeArtwork(Request $request, Collection $collection): JsonResponse { $this->authorize('manageArtworks', $collection); $artwork = $this->resolveArtworkFromRequest($request, 5); $isAttached = $collection->artworks()->where('artworks.id', $artwork->id)->exists(); abort_unless($isAttached, 404); $collection = $this->collections->removeArtwork($collection->loadMissing('user'), $artwork); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'availableArtworks' => $this->collections->getAvailableArtworkOptions($collection, $request->user()), ]); } public function reorderArtworks(ReorderCollectionArtworksRequest $request, Collection $collection): JsonResponse { $this->authorize('manageArtworks', $collection); $collection = $this->collections->reorderArtworks($collection->loadMissing('user'), $request->validated('ordered_artwork_ids')); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), ]); } public function availableArtworks(Request $request, Collection $collection): JsonResponse { $this->authorize('manageArtworks', $collection); $request->validate([ 'search' => ['nullable', 'string', 'max:120'], ]); return response()->json([ 'data' => $this->collections->getAvailableArtworkOptions($collection, $request->user(), (string) $request->input('search', '')), ]); } public function feature(Request $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->collections->featureCollection($collection); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), ]); } public function unfeature(Request $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->collections->unfeatureCollection($collection); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), ]); } public function reorderProfile(ReorderProfileCollectionsRequest $request): JsonResponse { $this->collections->reorderProfileCollections($request->user(), $request->validated('collection_ids')); $items = $this->collections->getProfileCollections($request->user(), $request->user(), 24); return response()->json([ 'ok' => true, 'collections' => $this->collections->mapCollectionCardPayloads($items, true), ]); } public function smartPreview(SmartCollectionRulesRequest $request): JsonResponse { return response()->json([ 'ok' => true, 'preview' => $this->collections->previewSmartCollection($request->user(), $request->validated('smart_rules_json')), ]); } public function updateSmartRules(SmartCollectionRulesRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection = $this->collections->updateCollection($collection->loadMissing('user'), [ 'title' => $collection->title, 'slug' => $collection->slug, 'description' => $collection->description, 'subtitle' => $collection->subtitle, 'summary' => $collection->summary, 'visibility' => $collection->visibility, 'mode' => Collection::MODE_SMART, 'sort_mode' => (string) (($request->validated('smart_rules_json')['sort'] ?? null) ?: $collection->sort_mode), 'smart_rules_json' => $request->validated('smart_rules_json'), ]); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'preview' => $this->collections->previewSmartCollection($request->user(), $collection->smart_rules_json ?? []), ]); } private function ownerPayload(Request $request): array { $user = $request->user(); return [ 'id' => $user->id, 'username' => $user->username, 'name' => $user->name, 'avatar_url' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 96), ]; } private function isAdmin(Request $request): bool { $user = $request->user(); return $user !== null && method_exists($user, 'hasRole') && $user->hasRole('admin'); } private function updateScopedSettings(Request $request, Collection $collection, array $attributes): JsonResponse { $collection = $this->collections->updateCollection($collection->loadMissing('user'), $attributes, $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'attachedArtworks' => $this->collections->mapAttachedArtworks($collection), 'members' => $this->collaborators->mapMembers($collection, $request->user()), 'submissions' => $this->submissions->mapSubmissions($collection, $request->user()), 'comments' => $this->comments->mapComments($collection, $request->user()), ]); } private function jsonOrRedirect(Request $request, array $payload, string $redirectTo): RedirectResponse|JsonResponse { if ($request->expectsJson()) { return response()->json($payload); } return redirect($redirectTo); } private function resolveArtworkFromRequest(Request $request, int $fallbackSegment): Artwork { $routeValue = $request->route('artwork'); if ($routeValue instanceof Artwork) { return $routeValue; } $artworkId = is_scalar($routeValue) ? (int) $routeValue : (int) $request->segment($fallbackSegment); return Artwork::query()->findOrFail($artworkId); } }