eligibleWorldsQuery() ->get() ->map(fn (World $world): array => $this->mapCreatorWorldOption($world, null, true)) ->all(); } public function artworkSubmissionOptions(Artwork $artwork, User $viewer): array { $artwork->loadMissing(['worldSubmissions.world', 'worldSubmissions.reviewer']); $existing = $artwork->worldSubmissions ->filter(fn (WorldSubmission $submission): bool => $submission->world !== null) ->keyBy(fn (WorldSubmission $submission): int => (int) $submission->world_id); $eligibleWorlds = $this->eligibleWorldsQuery()->get()->keyBy(fn (World $world): int => (int) $world->id); $worlds = $eligibleWorlds; $missingWorldIds = $existing->keys() ->map(fn ($id): int => (int) $id) ->reject(fn (int $id): bool => $eligibleWorlds->has($id)) ->values(); if ($missingWorldIds->isNotEmpty()) { World::query() ->whereIn('id', $missingWorldIds->all()) ->get() ->each(fn (World $world) => $worlds->put((int) $world->id, $world)); } return $worlds ->sortBy([ fn (World $world): int => $existing->has((int) $world->id) ? 0 : 1, fn (World $world): int => $world->starts_at?->getTimestamp() ?? PHP_INT_MAX, fn (World $world): string => Str::lower((string) $world->title), ]) ->values() ->map(function (World $world) use ($existing): array { $submission = $existing->get((int) $world->id); return $this->mapCreatorWorldOption($world, $submission, $this->isEligibleWorld($world)); }) ->all(); } public function syncForArtwork(Artwork $artwork, User $actor, array $entries): void { $artwork->loadMissing('worldSubmissions'); $normalizedEntries = collect($entries) ->map(function (array $entry): ?array { $worldId = (int) ($entry['world_id'] ?? 0); if ($worldId < 1) { return null; } return [ 'world_id' => $worldId, 'note' => Str::limit(trim((string) ($entry['note'] ?? '')), 1000, ''), ]; }) ->filter() ->unique('world_id') ->values(); $existing = $artwork->worldSubmissions->keyBy(fn (WorldSubmission $submission): int => (int) $submission->world_id); $selectedWorldIds = $normalizedEntries->pluck('world_id')->map(fn ($id): int => (int) $id)->all(); $allWorldIds = array_values(array_unique(array_merge($selectedWorldIds, $existing->keys()->map(fn ($id): int => (int) $id)->all()))); $worlds = World::query() ->whereIn('id', $allWorldIds) ->get() ->keyBy(fn (World $world): int => (int) $world->id); $errors = []; foreach ($normalizedEntries as $index => $entry) { $world = $worlds->get((int) $entry['world_id']); $submission = $existing->get((int) $entry['world_id']); if (! $world) { $errors["world_submissions.{$index}.world_id"] = 'Selected world no longer exists.'; continue; } if (! $this->isEligibleWorld($world)) { $errors["world_submissions.{$index}.world_id"] = 'That world is not currently accepting community submissions.'; continue; } if ($submission && $submission->isBlockingResubmission()) { $errors["world_submissions.{$index}.world_id"] = 'This artwork is blocked from that world until a moderator clears the block.'; continue; } if ($submission && (string) $submission->status === WorldSubmission::STATUS_REMOVED && ! (bool) $world->allow_readd_after_removal) { $errors["world_submissions.{$index}.world_id"] = 'That world does not allow re-adding removed artworks right now.'; } } if ($errors !== []) { throw ValidationException::withMessages($errors); } DB::transaction(function () use ($normalizedEntries, $artwork, $actor, $existing, $worlds, $selectedWorldIds): void { foreach ($normalizedEntries as $entry) { $worldId = (int) $entry['world_id']; $submission = $existing->get($worldId); $world = $worlds->get($worldId); $note = ($world?->submission_note_enabled ?? true) ? ($entry['note'] !== '' ? $entry['note'] : null) : null; $startingStatus = $world?->submissionStartsAsLive() ? WorldSubmission::STATUS_LIVE : WorldSubmission::STATUS_PENDING; $reviewedAt = $startingStatus === WorldSubmission::STATUS_LIVE ? now() : null; if ($submission && $submission->isBlockingResubmission()) { continue; } if ($submission) { $payload = [ 'mode_snapshot' => $world?->participation_mode, 'note' => $note, ]; if ((string) $submission->status === WorldSubmission::STATUS_REMOVED) { $payload = array_merge($payload, [ 'status' => $startingStatus, 'is_featured' => false, 'reviewer_note' => null, 'moderation_reason' => null, 'reviewed_by_user_id' => null, 'reviewed_at' => $reviewedAt, 'removed_at' => null, 'blocked_at' => null, 'featured_at' => null, ]); } $submission->forceFill($payload)->save(); continue; } WorldSubmission::query()->create([ 'world_id' => $worldId, 'artwork_id' => (int) $artwork->id, 'submitted_by_user_id' => (int) $actor->id, 'status' => $startingStatus, 'is_featured' => false, 'mode_snapshot' => $world?->participation_mode, 'note' => $note, 'reviewed_at' => $reviewedAt, ]); } $existing->each(function (WorldSubmission $submission, int $worldId) use ($selectedWorldIds): void { if (in_array((string) $submission->status, [WorldSubmission::STATUS_LIVE, WorldSubmission::STATUS_REMOVED, WorldSubmission::STATUS_BLOCKED], true)) { return; } if (! in_array($worldId, $selectedWorldIds, true)) { $submission->delete(); } }); }); } public function transition(WorldSubmission $submission, User $reviewer, string $status, ?string $reviewerNote = null): WorldSubmission { $payload = [ 'status' => $status, 'reviewer_note' => $this->nullableText($reviewerNote), 'moderation_reason' => $this->nullableText($reviewerNote), ]; if ($status === WorldSubmission::STATUS_PENDING) { $payload['reviewer_note'] = null; $payload['moderation_reason'] = null; $payload['reviewed_by_user_id'] = null; $payload['reviewed_at'] = null; $payload['removed_at'] = null; $payload['blocked_at'] = null; } else { $payload['reviewed_by_user_id'] = (int) $reviewer->id; $payload['reviewed_at'] = now(); $payload['removed_at'] = $status === WorldSubmission::STATUS_REMOVED ? now() : null; $payload['blocked_at'] = $status === WorldSubmission::STATUS_BLOCKED ? now() : null; } if ($status !== WorldSubmission::STATUS_LIVE) { $payload['is_featured'] = false; $payload['featured_at'] = null; } $submission->forceFill($payload)->save(); return $submission->fresh(['artwork.user.profile', 'artwork.stats', 'artwork.categories', 'submittedBy.profile', 'reviewer.profile']); } public function setFeatured(WorldSubmission $submission, User $reviewer, bool $featured, ?string $reviewerNote = null): WorldSubmission { $payload = [ 'is_featured' => $featured, 'featured_at' => $featured ? now() : null, 'reviewed_by_user_id' => (int) $reviewer->id, 'reviewed_at' => now(), ]; if ($reviewerNote !== null) { $payload['reviewer_note'] = $this->nullableText($reviewerNote); $payload['moderation_reason'] = $this->nullableText($reviewerNote); } if ((string) $submission->status !== WorldSubmission::STATUS_LIVE) { $payload['status'] = WorldSubmission::STATUS_LIVE; $payload['removed_at'] = null; $payload['blocked_at'] = null; } $submission->forceFill($payload)->save(); return $submission->fresh(['artwork.user.profile', 'artwork.stats', 'artwork.categories', 'submittedBy.profile', 'reviewer.profile']); } public function studioReviewQueue(World $world): array { $world->loadMissing([ 'worldSubmissions.artwork.user.profile', 'worldSubmissions.artwork.stats', 'worldSubmissions.artwork.categories', 'worldSubmissions.submittedBy.profile', 'worldSubmissions.reviewer.profile', ]); $items = $world->worldSubmissions ->sortBy([ fn (WorldSubmission $submission): int => match ((string) $submission->status) { WorldSubmission::STATUS_PENDING => 0, WorldSubmission::STATUS_LIVE => $submission->is_featured ? 1 : 2, WorldSubmission::STATUS_REMOVED => 3, WorldSubmission::STATUS_BLOCKED => 4, default => 4, }, fn (WorldSubmission $submission): int => -1 * ($submission->reviewed_at?->getTimestamp() ?? $submission->created_at?->getTimestamp() ?? 0), ]) ->values(); return [ 'counts' => [ 'pending' => $items->where('status', WorldSubmission::STATUS_PENDING)->count(), 'live' => $items->where('status', WorldSubmission::STATUS_LIVE)->count(), 'removed' => $items->where('status', WorldSubmission::STATUS_REMOVED)->count(), 'blocked' => $items->where('status', WorldSubmission::STATUS_BLOCKED)->count(), 'featured' => $items->where('is_featured', true)->count(), ], 'items' => $items->map(fn (WorldSubmission $submission): array => $this->mapStudioSubmission($submission))->all(), ]; } public function publicSectionPayload(World $world, ?User $viewer = null): ?array { if (! $world->community_section_enabled) { return null; } $query = Artwork::query() ->select('artworks.*', 'world_submissions.status as world_submission_status', 'world_submissions.is_featured as world_submission_is_featured', 'world_submissions.note as world_submission_note', 'world_submissions.reviewed_at as world_submission_reviewed_at') ->join('world_submissions', function ($join) use ($world): void { $join->on('world_submissions.artwork_id', '=', 'artworks.id') ->where('world_submissions.world_id', '=', $world->id) ->where('world_submissions.status', '=', WorldSubmission::STATUS_LIVE); }) ->with(['user.profile', 'categories.contentType', 'stats']) ->catalogVisible(); $this->maturity->applyViewerFilter($query, $viewer); $items = $query ->orderByRaw('CASE WHEN world_submissions.is_featured = 1 THEN 0 ELSE 1 END') ->orderByDesc('world_submissions.reviewed_at') ->limit(24) ->get() ->map(fn (Artwork $artwork): array => $this->mapPublicSubmissionArtwork($artwork)) ->all(); if ($items === []) { return null; } return [ 'title' => 'Community submissions', 'description' => 'Artworks submitted by creators and selected for this world outside the editorial curated-relation system.', 'items' => $items, ]; } private function eligibleWorldsQuery(): Builder { return World::query() ->published() ->where('accepts_submissions', true) ->whereIn('participation_mode', [World::PARTICIPATION_MODE_MANUAL_APPROVAL, World::PARTICIPATION_MODE_AUTO_ADD]) ->where(function (Builder $builder): void { $builder->whereNull('submission_starts_at') ->orWhere('submission_starts_at', '<=', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('submission_ends_at') ->orWhere('submission_ends_at', '>=', now()); }) ->orderBy('submission_ends_at') ->orderBy('starts_at') ->orderBy('title'); } private function isEligibleWorld(World $world): bool { return $world->isAcceptingSubmissions(); } private function mapCreatorWorldOption(World $world, ?WorldSubmission $submission, bool $eligible): array { $status = $submission ? (string) $submission->status : null; $selected = match ($status) { WorldSubmission::STATUS_PENDING, WorldSubmission::STATUS_LIVE => true, default => false, }; $locked = match ($status) { WorldSubmission::STATUS_BLOCKED => true, WorldSubmission::STATUS_PENDING => ! $eligible, WorldSubmission::STATUS_REMOVED => ! $eligible || ! (bool) $world->allow_readd_after_removal, default => false, }; $lockedReason = $locked ? match ($status) { WorldSubmission::STATUS_BLOCKED => 'This artwork is blocked from this world until a moderator clears the block.', WorldSubmission::STATUS_PENDING => 'This world is no longer accepting submission changes right now.', WorldSubmission::STATUS_REMOVED => (bool) $world->allow_readd_after_removal ? 'This world is not currently open for re-adding removed artworks.' : 'Removed artworks cannot be re-added to this world right now.', default => 'This world is locked.', } : null; return [ 'id' => (int) $world->id, 'title' => (string) $world->title, 'slug' => (string) $world->slug, 'tagline' => (string) ($world->tagline ?? ''), 'summary' => (string) ($world->summary ?? ''), 'cover_url' => $world->coverUrl(), 'timeframe_label' => $this->timeframeLabel($world), 'submission_window_label' => $this->submissionWindowLabel($world), 'submission_guidelines' => (string) ($world->submission_guidelines ?? ''), 'participation_mode' => (string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED), 'participation_mode_label' => $this->participationModeLabel((string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED)), 'submission_note_enabled' => (bool) $world->submission_note_enabled, 'is_accepting_submissions' => $eligible, 'selected' => $selected, 'selection_locked' => $locked, 'selection_locked_reason' => $lockedReason, 'note' => (string) ($submission?->note ?? ''), 'status' => $status, 'status_label' => $status ? $this->statusLabel($status, (bool) ($submission?->is_featured ?? false)) : null, 'reviewer_note' => (string) ($submission?->moderation_reason ?: $submission?->reviewer_note ?? ''), 'is_featured' => (bool) ($submission?->is_featured ?? false), 'submitted_at' => $submission?->created_at?->toIso8601String(), 'reviewed_at' => $submission?->reviewed_at?->toIso8601String(), 'can_resubmit' => $eligible && (bool) $world->allow_readd_after_removal && $status === WorldSubmission::STATUS_REMOVED, ]; } private function mapStudioSubmission(WorldSubmission $submission): array { $artwork = $submission->artwork; $views = (int) ($artwork?->stats?->views ?? 0); return [ 'id' => (int) $submission->id, 'status' => (string) $submission->status, 'status_label' => $this->statusLabel((string) $submission->status, (bool) $submission->is_featured), 'is_featured' => (bool) $submission->is_featured, 'note' => (string) ($submission->note ?? ''), 'reviewer_note' => (string) ($submission->moderation_reason ?: $submission->reviewer_note ?? ''), 'submitted_at' => $submission->created_at?->toIso8601String(), 'reviewed_at' => $submission->reviewed_at?->toIso8601String(), 'removed_at' => $submission->removed_at?->toIso8601String(), 'blocked_at' => $submission->blocked_at?->toIso8601String(), 'featured_at' => $submission->featured_at?->toIso8601String(), 'submitted_by' => $submission->submittedBy ? [ 'id' => (int) $submission->submittedBy->id, 'name' => (string) ($submission->submittedBy->name ?: $submission->submittedBy->username ?: 'Unknown creator'), 'username' => (string) ($submission->submittedBy->username ?? ''), ] : null, 'reviewed_by' => $submission->reviewer ? [ 'id' => (int) $submission->reviewer->id, 'name' => (string) ($submission->reviewer->name ?: $submission->reviewer->username ?: 'Moderator'), ] : null, 'artwork' => $artwork ? [ 'id' => (int) $artwork->id, 'title' => (string) ($artwork->title ?: 'Untitled artwork'), 'slug' => (string) ($artwork->slug ?? ''), 'url' => route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)]), 'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]), 'thumbnail_url' => $artwork->thumbUrl('md'), 'creator_name' => (string) ($artwork->user?->name ?: $artwork->user?->username ?: ''), 'meta' => array_values(array_filter([ $artwork->categories->first()?->name, $views > 0 ? number_format($views) . ' views' : null, $artwork->visibility ? Str::headline((string) $artwork->visibility) : null, ])), ] : null, 'actions' => [ 'approve' => route('studio.worlds.submissions.approve', ['world' => $submission->world_id, 'submission' => $submission->id]), 'remove' => route('studio.worlds.submissions.remove', ['world' => $submission->world_id, 'submission' => $submission->id]), 'block' => route('studio.worlds.submissions.block', ['world' => $submission->world_id, 'submission' => $submission->id]), 'unblock' => route('studio.worlds.submissions.unblock', ['world' => $submission->world_id, 'submission' => $submission->id]), 'restore' => route('studio.worlds.submissions.restore', ['world' => $submission->world_id, 'submission' => $submission->id]), 'feature' => route('studio.worlds.submissions.feature', ['world' => $submission->world_id, 'submission' => $submission->id]), 'unfeature' => route('studio.worlds.submissions.unfeature', ['world' => $submission->world_id, 'submission' => $submission->id]), 'pending' => route('studio.worlds.submissions.pending', ['world' => $submission->world_id, 'submission' => $submission->id]), ], ]; } private function mapPublicSubmissionArtwork(Artwork $artwork): array { $resource = ArtworkListResource::make($artwork)->toArray(request()); $views = (int) ($artwork->stats?->views ?? 0); $status = (string) ($artwork->world_submission_status ?? WorldSubmission::STATUS_LIVE); $isFeatured = (bool) ($artwork->world_submission_is_featured ?? false); return [ 'id' => (int) $artwork->id, 'title' => (string) ($resource['title'] ?? $artwork->title ?? 'Untitled artwork'), 'subtitle' => (string) ($resource['author']['name'] ?? ''), 'description' => Str::limit(trim(strip_tags((string) ($artwork->description ?? ''))), 120), 'url' => (string) ($resource['urls']['canonical'] ?? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)])), 'image' => $resource['thumbnail_url'] ?? $artwork->thumbUrl('md'), 'status' => $status, 'status_label' => $this->statusLabel($status, $isFeatured), 'context_label' => $isFeatured ? 'Community featured' : 'Community submission', 'meta' => array_values(array_filter([ $resource['category']['name'] ?? null, $views > 0 ? number_format($views) . ' views' : null, ])), ]; } private function statusLabel(string $status, bool $isFeatured = false): string { if ($status === WorldSubmission::STATUS_LIVE && $isFeatured) { return 'Featured'; } return match ($status) { WorldSubmission::STATUS_PENDING => 'Pending', WorldSubmission::STATUS_LIVE => 'Live', WorldSubmission::STATUS_REMOVED => 'Removed', WorldSubmission::STATUS_BLOCKED => 'Blocked', default => Str::headline($status), }; } private function participationModeLabel(string $mode): string { return match ($mode) { World::PARTICIPATION_MODE_MANUAL_APPROVAL => 'Manual approval', World::PARTICIPATION_MODE_AUTO_ADD => 'Auto add', World::PARTICIPATION_MODE_CLOSED => 'Closed', default => Str::headline($mode), }; } private function timeframeLabel(World $world): string { if ($world->starts_at && $world->ends_at) { return $world->starts_at->format('M j') . ' - ' . $world->ends_at->format('M j, Y'); } if ($world->starts_at) { return 'Starts ' . $world->starts_at->format('M j, Y'); } if ($world->ends_at) { return 'Until ' . $world->ends_at->format('M j, Y'); } return 'Open-ended world'; } private function submissionWindowLabel(World $world): string { $start = $world->submission_starts_at; $end = $world->submission_ends_at; if ($start && $end) { return $start->format('M j') . ' - ' . $end->format('M j, Y'); } if ($start) { return 'Opens ' . $start->format('M j, Y'); } if ($end) { return 'Open until ' . $end->format('M j, Y'); } return 'Open submissions'; } private function nullableText(?string $value): ?string { $value = trim((string) $value); return $value !== '' ? $value : null; } }