map(fn (string $label, string $value): array => ['value' => $value, 'label' => $label]) ->values() ->all(); } public function sectionOptions(): array { return collect((array) config('worlds.sections', [])) ->map(fn (array $config, string $key): array => [ 'value' => $key, 'label' => (string) ($config['label'] ?? Str::headline($key)), 'description' => (string) ($config['description'] ?? ''), 'relation_types' => array_values((array) ($config['relation_types'] ?? [])), ]) ->values() ->all(); } public function themeOptions(): array { return collect((array) config('worlds.themes', [])) ->map(fn (array $theme, string $key): array => [ 'value' => $key, 'label' => (string) ($theme['label'] ?? Str::headline($key)), 'accent_color' => $theme['accent_color'] ?? null, 'accent_color_secondary' => $theme['accent_color_secondary'] ?? null, 'background_motif' => $theme['background_motif'] ?? null, 'icon_name' => $theme['icon_name'] ?? null, 'related_tags_json' => array_values(array_map('strval', (array) ($theme['related_tags_json'] ?? []))), 'suggested_badge_label' => (string) ($theme['suggested_badge_label'] ?? ''), 'suggested_cta_label' => (string) ($theme['suggested_cta_label'] ?? ''), ]) ->values() ->all(); } public function studioListing(array $filters = []): array { $query = World::query()->withCount('worldRelations')->orderByDesc('is_featured')->orderByRaw("CASE WHEN status = 'published' THEN 0 WHEN status = 'draft' THEN 1 ELSE 2 END")->orderByDesc('starts_at')->orderByDesc('published_at'); $search = trim((string) ($filters['q'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $type = trim((string) ($filters['type'] ?? '')); $perPage = max(10, min(50, (int) ($filters['per_page'] ?? 15))); if ($search !== '') { $query->where(function (Builder $builder) use ($search): void { $builder->where('title', 'like', '%' . $search . '%') ->orWhere('slug', 'like', '%' . $search . '%') ->orWhere('summary', 'like', '%' . $search . '%') ->orWhere('description', 'like', '%' . $search . '%'); }); } if ($status !== '') { $query->where('status', $status); } if ($type !== '') { $query->where('type', $type); } $paginator = $query->paginate($perPage)->withQueryString(); return [ 'items' => $paginator->getCollection()->map(fn (World $world): array => $this->mapStudioListItem($world))->all(), 'meta' => $this->paginationMeta($paginator), 'filters' => [ 'q' => $search, 'status' => $status, 'type' => $type, 'per_page' => $perPage, ], ]; } public function mapStudioWorld(World $world, ?User $viewer = null): array { $world->loadMissing(['createdBy.profile', 'parentWorld', 'worldRelations']); return [ 'id' => (int) $world->id, 'title' => (string) $world->title, 'slug' => (string) $world->slug, 'tagline' => (string) ($world->tagline ?? ''), 'summary' => (string) ($world->summary ?? ''), 'description' => (string) ($world->description ?? ''), 'cover_path' => (string) ($world->cover_path ?? ''), 'cover_url' => $world->coverUrl(), 'theme_key' => (string) ($world->theme_key ?? ''), 'theme' => $this->themePayload($world), 'accent_color' => (string) ($world->accent_color ?? ''), 'accent_color_secondary' => (string) ($world->accent_color_secondary ?? ''), 'background_motif' => (string) ($world->background_motif ?? ''), 'icon_name' => $this->resolvedIconName($world), 'status' => (string) $world->status, 'type' => (string) $world->type, 'starts_at' => optional($world->starts_at)?->toIso8601String(), 'ends_at' => optional($world->ends_at)?->toIso8601String(), 'accepts_submissions' => (bool) $world->accepts_submissions, 'participation_mode' => (string) ($world->participation_mode ?: World::PARTICIPATION_MODE_CLOSED), 'submission_starts_at' => optional($world->submission_starts_at)?->toIso8601String(), 'submission_ends_at' => optional($world->submission_ends_at)?->toIso8601String(), 'submission_note_enabled' => (bool) $world->submission_note_enabled, 'community_section_enabled' => (bool) $world->community_section_enabled, 'allow_readd_after_removal' => (bool) $world->allow_readd_after_removal, 'is_featured' => (bool) $world->is_featured, 'is_recurring' => (bool) $world->is_recurring, 'recurrence_key' => (string) ($world->recurrence_key ?? ''), 'recurrence_rule' => (string) ($world->recurrence_rule ?? ''), 'edition_year' => $world->edition_year, 'cta_label' => (string) ($world->cta_label ?? ''), 'cta_url' => (string) ($world->cta_url ?? ''), 'badge_label' => (string) ($world->badge_label ?? ''), 'badge_description' => (string) ($world->badge_description ?? ''), 'submission_guidelines' => (string) ($world->submission_guidelines ?? ''), 'badge_url' => (string) ($world->badge_url ?? ''), 'seo_title' => (string) ($world->seo_title ?? ''), 'seo_description' => (string) ($world->seo_description ?? ''), 'og_image_path' => (string) ($world->og_image_path ?? ''), 'og_image_url' => $world->ogImageUrl(), 'published_at' => optional($world->published_at)?->toIso8601String(), 'parent_world_id' => $world->parent_world_id ? (int) $world->parent_world_id : null, 'parent_world' => $world->parentWorld ? [ 'id' => (int) $world->parentWorld->id, 'title' => (string) $world->parentWorld->title, 'slug' => (string) $world->parentWorld->slug, ] : null, 'related_tags_json' => array_values(array_map('strval', $world->related_tags_json ?? [])), 'section_order_json' => $world->sectionOrder(), 'section_visibility_json' => $world->sectionVisibility(), 'relations' => $world->worldRelations ->values() ->map(fn (WorldRelation $relation): array => [ 'id' => (int) $relation->id, 'section_key' => (string) $relation->section_key, 'related_type' => (string) $relation->related_type, 'related_id' => (int) $relation->related_id, 'context_label' => (string) ($relation->context_label ?? ''), 'sort_order' => (int) $relation->sort_order, 'is_featured' => (bool) $relation->is_featured, 'preview' => $this->resolveEntityPreview((string) $relation->related_type, (int) $relation->related_id, $viewer, (string) ($relation->context_label ?? '')), ]) ->all(), 'created_by' => $world->createdBy ? [ 'id' => (int) $world->createdBy->id, 'name' => (string) $world->createdBy->name, 'username' => (string) ($world->createdBy->username ?? ''), ] : null, 'submission_review_queue' => $this->submissions->studioReviewQueue($world), 'urls' => [ 'public' => $world->publicUrl(), 'edit' => route('studio.worlds.edit', ['world' => $world]), 'preview' => route('studio.worlds.preview', ['world' => $world]), 'publish' => route('studio.worlds.publish', ['world' => $world]), 'archive' => route('studio.worlds.archive', ['world' => $world]), 'duplicate' => route('studio.worlds.duplicate', ['world' => $world]), 'new_edition' => route('studio.worlds.new-edition', ['world' => $world]), ], ]; } public function store(User $editor, array $data): World { $world = new World(); return $this->persist($world, $editor, $data); } public function update(World $world, User $editor, array $data): World { return $this->persist($world, $editor, $data); } public function duplicate(World $source, User $editor, bool $asNewEdition = false): World { $source->loadMissing('worldRelations'); $data = [ 'title' => $asNewEdition ? $this->nextEditionTitle($source) : $this->duplicateTitle($source), 'slug' => $asNewEdition ? $this->nextEditionSlug($source) : $source->slug . '-copy', 'tagline' => $source->tagline, 'summary' => $source->summary, 'description' => $source->description, 'cover_path' => $source->cover_path, 'theme_key' => $source->theme_key, 'accent_color' => $source->accent_color, 'accent_color_secondary' => $source->accent_color_secondary, 'background_motif' => $source->background_motif, 'icon_name' => $source->icon_name, 'status' => World::STATUS_DRAFT, 'type' => $source->type, 'starts_at' => null, 'ends_at' => null, 'accepts_submissions' => (bool) $source->accepts_submissions, 'participation_mode' => (string) ($source->participation_mode ?: World::PARTICIPATION_MODE_CLOSED), 'submission_starts_at' => $source->submission_starts_at, 'submission_ends_at' => $source->submission_ends_at, 'submission_note_enabled' => (bool) $source->submission_note_enabled, 'community_section_enabled' => (bool) $source->community_section_enabled, 'allow_readd_after_removal' => (bool) $source->allow_readd_after_removal, 'published_at' => null, 'is_featured' => false, 'is_recurring' => $asNewEdition ? true : $source->is_recurring, 'recurrence_key' => $asNewEdition ? ($source->recurrence_key ?: Str::slug((string) $source->slug)) : $source->recurrence_key, 'recurrence_rule' => $source->recurrence_rule, 'edition_year' => $asNewEdition ? $this->nextEditionYear($source) : $source->edition_year, 'cta_label' => $source->cta_label, 'cta_url' => $source->cta_url, 'badge_label' => $source->badge_label, 'badge_description' => $source->badge_description, 'submission_guidelines' => $source->submission_guidelines, 'badge_url' => $source->badge_url, 'seo_title' => $source->seo_title, 'seo_description' => $source->seo_description, 'og_image_path' => $source->og_image_path, 'related_tags_json' => array_values(array_map('strval', $source->related_tags_json ?? [])), 'section_order_json' => $source->sectionOrder(), 'section_visibility_json' => $source->sectionVisibility(), 'parent_world_id' => $asNewEdition ? (int) ($source->parent_world_id ?: $source->id) : $source->parent_world_id, 'relations' => $source->worldRelations ->map(fn (WorldRelation $relation): array => [ 'section_key' => (string) $relation->section_key, 'related_type' => (string) $relation->related_type, 'related_id' => (int) $relation->related_id, 'context_label' => (string) ($relation->context_label ?? ''), 'sort_order' => (int) $relation->sort_order, 'is_featured' => (bool) $relation->is_featured, ]) ->all(), ]; return $this->store($editor, $data); } public function canCreateNewEdition(World $world): bool { return (bool) ($world->is_recurring || $world->recurrence_key || $world->edition_year); } public function publish(World $world): World { $world->forceFill([ 'status' => World::STATUS_PUBLISHED, 'published_at' => $world->published_at ?? now(), ])->save(); return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations']); } public function archive(World $world): World { $world->forceFill([ 'status' => World::STATUS_ARCHIVED, 'ends_at' => $world->ends_at ?? now(), ])->save(); return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations']); } public function searchEntities(string $type, string $query, ?User $viewer = null): array { $type = trim(Str::lower($type)); $query = trim($query); return match ($type) { WorldRelation::TYPE_ARTWORK => $this->searchArtworks($query), WorldRelation::TYPE_COLLECTION => $this->searchCollections($query, $viewer), WorldRelation::TYPE_USER => $this->searchUsers($query), WorldRelation::TYPE_GROUP => $this->searchGroups($query, $viewer), WorldRelation::TYPE_NEWS => $this->searchNews($query), WorldRelation::TYPE_CHALLENGE => $this->searchChallenges($query, $viewer), WorldRelation::TYPE_EVENT => $this->searchEvents($query, $viewer), WorldRelation::TYPE_RELEASE => $this->searchReleases($query, $viewer), WorldRelation::TYPE_CARD => $this->searchCards($query, $viewer), default => [], }; } public function publicIndexPayload(?User $viewer = null): array { $featuredWorld = World::query()->current()->where('is_featured', true)->latest('starts_at')->first() ?? World::query()->published()->where('is_featured', true)->latest('published_at')->first(); $current = World::query()->current()->orderByDesc('is_featured')->orderBy('starts_at')->limit(12)->get(); $upcoming = World::query()->upcoming()->orderBy('starts_at')->limit(12)->get(); $archive = World::query()->archive()->orderByDesc('ends_at')->orderByDesc('published_at')->limit(18)->get(); return [ 'title' => 'Worlds', 'description' => 'Seasonal, cultural, and editorial campaign spaces that bring together artworks, collections, creators, groups, news, challenges, and releases.', 'featuredWorld' => $featuredWorld ? $this->mapWorldCard($featuredWorld, 'featured') : null, 'activeWorlds' => $current->map(fn (World $world): array => $this->mapWorldCard($world, 'active'))->all(), 'upcomingWorlds' => $upcoming->map(fn (World $world): array => $this->mapWorldCard($world, 'upcoming'))->all(), 'archivedWorlds' => $archive->map(fn (World $world): array => $this->mapWorldCard($world, 'archive'))->all(), 'themeOptions' => $this->themeOptions(), ]; } public function publicShowPayload(World $world, ?User $viewer = null): array { $world->loadMissing(['createdBy.profile', 'parentWorld', 'archiveEditions', 'worldRelations']); $sections = $this->resolveSections($world, $viewer); $archiveEditions = $world->archiveEditions ->filter(fn (World $edition): bool => $edition->isPubliclyVisible()) ->sortByDesc(fn (World $edition): int => (int) ($edition->edition_year ?? 0)) ->values() ->map(fn (World $edition): array => $this->mapWorldCard($edition, 'archive')) ->all(); $relatedWorlds = World::query() ->publiclyVisible() ->when($world->recurrence_key, fn (Builder $builder) => $builder->where('recurrence_key', $world->recurrence_key)) ->where('id', '!=', $world->id) ->orderByDesc('starts_at') ->limit(3) ->get() ->map(fn (World $item): array => $this->mapWorldCard($item, $item->isCurrent() ? 'active' : 'archive')) ->all(); return [ 'world' => $this->mapWorldDetail($world), 'sections' => $sections, 'communitySubmissions' => $this->submissions->publicSectionPayload($world, $viewer), 'archiveEditions' => $archiveEditions, 'relatedWorlds' => $relatedWorlds, ]; } public function homepageSpotlight(?User $viewer = null): ?array { $world = World::query()->current()->where('is_featured', true)->latest('starts_at')->first() ?? World::query()->current()->latest('starts_at')->first() ?? World::query()->upcoming()->where('is_featured', true)->orderBy('starts_at')->first(); if (! $world) { return null; } $payload = $this->mapWorldDetail($world); return [ 'id' => $payload['id'], 'title' => $payload['title'], 'tagline' => $payload['tagline'], 'summary' => $payload['summary'], 'cover_url' => $payload['cover_url'], 'icon_name' => $payload['icon_name'], 'badge_label' => $payload['badge_label'], 'timeframe_label' => $payload['timeframe_label'], 'theme' => $payload['theme'], 'cta_label' => $payload['cta_label'] ?: 'Explore world', 'cta_url' => $payload['public_url'], 'public_url' => $payload['public_url'], ]; } private function persist(World $world, User $editor, array $data): World { $originalCoverPath = (string) ($world->cover_path ?? ''); $originalOgImagePath = (string) ($world->og_image_path ?? ''); $title = trim((string) ($data['title'] ?? $world->title ?? 'Untitled World')); $status = (string) ($data['status'] ?? $world->status ?? World::STATUS_DRAFT); $publishedAt = $this->normalizePublishedAt($status, $data['published_at'] ?? $world->published_at); $world->fill([ 'title' => $title, 'slug' => $this->resolveSlug($title, $world, $data), 'tagline' => $this->nullableText($data['tagline'] ?? null), 'summary' => $this->nullableText($data['summary'] ?? null), 'description' => $this->nullableText($data['description'] ?? null), 'cover_path' => $this->nullableText($data['cover_path'] ?? null), 'theme_key' => $this->nullableText($data['theme_key'] ?? null), 'accent_color' => $this->nullableText($data['accent_color'] ?? null), 'accent_color_secondary' => $this->nullableText($data['accent_color_secondary'] ?? null), 'background_motif' => $this->nullableText($data['background_motif'] ?? null), 'icon_name' => $this->nullableText($data['icon_name'] ?? null), 'status' => $status, 'type' => (string) ($data['type'] ?? World::TYPE_SEASONAL), 'starts_at' => ! empty($data['starts_at']) ? Carbon::parse((string) $data['starts_at']) : null, 'ends_at' => ! empty($data['ends_at']) ? Carbon::parse((string) $data['ends_at']) : null, 'accepts_submissions' => (bool) ($data['accepts_submissions'] ?? false), 'participation_mode' => (string) ($data['participation_mode'] ?? ((bool) ($data['accepts_submissions'] ?? false) ? World::PARTICIPATION_MODE_MANUAL_APPROVAL : World::PARTICIPATION_MODE_CLOSED)), 'submission_starts_at' => ! empty($data['submission_starts_at']) ? Carbon::parse((string) $data['submission_starts_at']) : null, 'submission_ends_at' => ! empty($data['submission_ends_at']) ? Carbon::parse((string) $data['submission_ends_at']) : null, 'submission_note_enabled' => (bool) ($data['submission_note_enabled'] ?? true), 'community_section_enabled' => (bool) ($data['community_section_enabled'] ?? true), 'allow_readd_after_removal' => (bool) ($data['allow_readd_after_removal'] ?? true), 'is_featured' => (bool) ($data['is_featured'] ?? false), 'is_recurring' => (bool) ($data['is_recurring'] ?? false), 'recurrence_key' => $this->nullableText($data['recurrence_key'] ?? null), 'recurrence_rule' => $this->nullableText($data['recurrence_rule'] ?? null), 'edition_year' => ! empty($data['edition_year']) ? (int) $data['edition_year'] : null, 'cta_label' => $this->nullableText($data['cta_label'] ?? null), 'cta_url' => $this->nullableText($data['cta_url'] ?? null), 'badge_label' => $this->nullableText($data['badge_label'] ?? null), 'badge_description' => $this->nullableText($data['badge_description'] ?? null), 'submission_guidelines' => $this->nullableText($data['submission_guidelines'] ?? null), 'badge_url' => $this->nullableText($data['badge_url'] ?? null), 'seo_title' => $this->nullableText($data['seo_title'] ?? null), 'seo_description' => $this->nullableText($data['seo_description'] ?? null), 'og_image_path' => $this->nullableText($data['og_image_path'] ?? null), 'related_tags_json' => collect((array) ($data['related_tags_json'] ?? []))->map(fn ($tag) => trim((string) $tag))->filter()->values()->all(), 'section_order_json' => $this->normalizeSectionOrder($data['section_order_json'] ?? []), 'section_visibility_json' => $this->normalizeSectionVisibility($data['section_visibility_json'] ?? []), 'parent_world_id' => ! empty($data['parent_world_id']) ? (int) $data['parent_world_id'] : null, 'published_at' => $publishedAt, ]); if ((string) $world->participation_mode === World::PARTICIPATION_MODE_CLOSED) { $world->accepts_submissions = false; } if (! $world->exists) { $world->created_by_user_id = (int) $editor->id; } $world->save(); $this->deleteWorldMediaIfReplaced($originalCoverPath, (string) ($world->cover_path ?? '')); $this->deleteWorldMediaIfReplaced($originalOgImagePath, (string) ($world->og_image_path ?? '')); $this->syncRelations($world, (array) ($data['relations'] ?? [])); return $world->fresh(['createdBy.profile', 'parentWorld', 'worldRelations']); } private function syncRelations(World $world, array $relations): void { $normalized = collect($relations) ->map(function (array $relation): ?array { $sectionKey = trim((string) ($relation['section_key'] ?? '')); $relatedType = trim((string) ($relation['related_type'] ?? '')); $relatedId = (int) ($relation['related_id'] ?? 0); if ($sectionKey === '' || $relatedType === '' || $relatedId < 1) { return null; } return [ 'section_key' => $sectionKey, 'related_type' => $relatedType, 'related_id' => $relatedId, 'context_label' => Str::limit(trim((string) ($relation['context_label'] ?? '')), 120, ''), 'sort_order' => max(0, (int) ($relation['sort_order'] ?? 0)), 'is_featured' => (bool) ($relation['is_featured'] ?? false), ]; }) ->filter() ->sortBy(fn (array $relation): string => sprintf('%s:%09d:%s:%09d', $relation['section_key'], (int) $relation['sort_order'], $relation['related_type'], (int) $relation['related_id'])) ->values(); $world->worldRelations()->delete(); foreach ($normalized as $index => $relation) { $world->worldRelations()->create([ 'section_key' => $relation['section_key'], 'related_type' => $relation['related_type'], 'related_id' => $relation['related_id'], 'context_label' => $relation['context_label'] !== '' ? $relation['context_label'] : null, 'sort_order' => $relation['sort_order'] ?: $index, 'is_featured' => $relation['is_featured'], ]); } } private function resolveSections(World $world, ?User $viewer = null): array { $relations = $world->worldRelations->values(); if ($relations->isEmpty()) { return []; } $entities = $this->resolveEntitiesForRelations($relations, $viewer); $sectionConfigs = (array) config('worlds.sections', []); $sectionVisibility = $world->sectionVisibility(); $sections = []; foreach ($world->sectionOrder() as $sectionKey) { if (($sectionVisibility[$sectionKey] ?? true) !== true) { continue; } $items = $relations ->where('section_key', $sectionKey) ->map(function (WorldRelation $relation) use ($entities): ?array { $item = $entities->get($this->entityMapKey((string) $relation->related_type, (int) $relation->related_id)); if (! $item) { return null; } return array_merge($item, [ 'context_label' => trim((string) ($relation->context_label ?? '')) !== '' ? (string) $relation->context_label : ($item['context_label'] ?? null), 'is_featured' => (bool) $relation->is_featured, ]); }) ->filter() ->values() ->all(); if ($items === []) { continue; } $sections[] = [ 'key' => $sectionKey, 'title' => (string) ($sectionConfigs[$sectionKey]['label'] ?? Str::headline($sectionKey)), 'description' => (string) ($sectionConfigs[$sectionKey]['description'] ?? ''), 'items' => $items, ]; } return $sections; } private function resolveEntitiesForRelations(SupportCollection $relations, ?User $viewer = null): SupportCollection { $map = collect(); $grouped = $relations->groupBy('related_type'); if ($grouped->has(WorldRelation::TYPE_ARTWORK)) { $ids = $grouped[WorldRelation::TYPE_ARTWORK]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $artworks = Artwork::query() ->with(['user.profile', 'categories.contentType']) ->whereIn('id', $ids) ->where('artwork_status', 'published') ->where('visibility', Artwork::VISIBILITY_PUBLIC) ->get(); foreach ($artworks as $artwork) { $preview = $this->resolveArtworkPreview((int) $artwork->id, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_ARTWORK, (int) $artwork->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_COLLECTION)) { $ids = $grouped[WorldRelation::TYPE_COLLECTION]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $collections = Collection::query()->with(['user.profile', 'group', 'coverArtwork'])->whereIn('id', $ids)->get(); $payloads = collect($this->collections->mapCollectionCardPayloads($collections, false, $viewer))->keyBy(fn (array $item): int => (int) $item['id']); foreach ($ids as $id) { $payload = $payloads->get($id); if ($payload) { $map->put($this->entityMapKey(WorldRelation::TYPE_COLLECTION, $id), array_merge($payload, [ 'entity_type' => WorldRelation::TYPE_COLLECTION, 'entity_label' => (string) (config('worlds.relation_types.collection') ?? 'Collection'), ])); } } } if ($grouped->has(WorldRelation::TYPE_USER)) { $ids = $grouped[WorldRelation::TYPE_USER]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $users = User::query()->with('profile')->whereIn('id', $ids)->get(); foreach ($users as $user) { $preview = $this->resolveUserPreview((int) $user->id, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_USER, (int) $user->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_GROUP)) { $ids = $grouped[WorldRelation::TYPE_GROUP]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $groups = Group::query()->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges'])->whereIn('id', $ids)->get(); foreach ($groups as $group) { if ($group->canBeViewedBy($viewer)) { $map->put($this->entityMapKey(WorldRelation::TYPE_GROUP, (int) $group->id), array_merge($this->groups->mapGroupCard($group, $viewer), [ 'entity_type' => WorldRelation::TYPE_GROUP, 'entity_label' => (string) (config('worlds.relation_types.group') ?? 'Group'), ])); } } } if ($grouped->has(WorldRelation::TYPE_NEWS)) { $ids = $grouped[WorldRelation::TYPE_NEWS]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $articles = NewsArticle::query()->with(['author.profile', 'category'])->published()->whereIn('id', $ids)->get(); foreach ($articles as $article) { $preview = $this->resolveNewsPreview((int) $article->id, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_NEWS, (int) $article->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_CHALLENGE)) { $ids = $grouped[WorldRelation::TYPE_CHALLENGE]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $items = GroupChallenge::query()->with('group')->whereIn('id', $ids)->get(); foreach ($items as $item) { $preview = $this->resolveChallengePreview((int) $item->id, $viewer, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_CHALLENGE, (int) $item->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_EVENT)) { $ids = $grouped[WorldRelation::TYPE_EVENT]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $items = GroupEvent::query()->with('group')->whereIn('id', $ids)->get(); foreach ($items as $item) { $preview = $this->resolveEventPreview((int) $item->id, $viewer, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_EVENT, (int) $item->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_RELEASE)) { $ids = $grouped[WorldRelation::TYPE_RELEASE]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $items = GroupRelease::query()->with('group')->whereIn('id', $ids)->get(); foreach ($items as $item) { $preview = $this->resolveReleasePreview((int) $item->id, $viewer, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_RELEASE, (int) $item->id), $preview); } } } if ($grouped->has(WorldRelation::TYPE_CARD)) { $ids = $grouped[WorldRelation::TYPE_CARD]->pluck('related_id')->map(fn ($id) => (int) $id)->all(); $items = NovaCard::query()->with(['user.profile', 'category'])->whereIn('id', $ids)->get(); foreach ($items as $item) { $preview = $this->resolveCardPreview((int) $item->id, $viewer, ''); if ($preview) { $map->put($this->entityMapKey(WorldRelation::TYPE_CARD, (int) $item->id), $preview); } } } return $map; } private function mapWorldCard(World $world, string $phase): array { $detail = $this->mapWorldDetail($world); return [ 'id' => $detail['id'], 'title' => $detail['title'], 'slug' => $detail['slug'], 'tagline' => $detail['tagline'], 'summary' => $detail['summary'], 'cover_url' => $detail['cover_url'], 'theme' => $detail['theme'], 'type' => $detail['type'], 'status' => $detail['status'], 'phase' => $phase, 'badge_label' => $detail['badge_label'], 'icon_name' => $detail['icon_name'], 'timeframe_label' => $detail['timeframe_label'], 'starts_at' => $detail['starts_at'], 'ends_at' => $detail['ends_at'], 'public_url' => $detail['public_url'], 'cta_label' => $detail['cta_label'], 'is_featured' => $detail['is_featured'], ]; } private function mapWorldDetail(World $world): array { $theme = $this->themePayload($world); return [ 'id' => (int) $world->id, 'title' => (string) $world->title, 'slug' => (string) $world->slug, 'tagline' => (string) ($world->tagline ?? ''), 'summary' => (string) ($world->summary ?? ''), 'description' => (string) ($world->description ?? ''), 'cover_url' => $world->coverUrl(), 'type' => (string) $world->type, 'status' => (string) $world->status, 'theme' => $theme, 'icon_name' => $this->resolvedIconName($world, $theme), 'badge_label' => (string) ($world->badge_label ?? ''), 'badge_description' => (string) ($world->badge_description ?? ''), 'badge_url' => (string) ($world->badge_url ?? ''), 'cta_label' => (string) ($world->cta_label ?? ''), 'cta_url' => (string) ($world->cta_url ?? ''), 'starts_at' => optional($world->starts_at)?->toIso8601String(), 'ends_at' => optional($world->ends_at)?->toIso8601String(), 'timeframe_label' => $this->timeframeLabel($world), 'related_tags' => array_values(array_map('strval', $world->related_tags_json ?? [])), 'recurrence_key' => (string) ($world->recurrence_key ?? ''), 'edition_year' => $world->edition_year, 'is_recurring' => (bool) $world->is_recurring, 'is_featured' => (bool) $world->is_featured, 'public_url' => $world->publicUrl(), ]; } private function mapStudioListItem(World $world): array { return [ 'id' => (int) $world->id, 'title' => (string) $world->title, 'slug' => (string) $world->slug, 'status' => (string) $world->status, 'type' => (string) $world->type, 'theme_key' => (string) ($world->theme_key ?? ''), 'summary' => Str::limit(trim(strip_tags((string) ($world->summary ?: $world->description ?: ''))), 120), 'timeframe_label' => $this->timeframeLabel($world), 'relation_count' => (int) ($world->world_relations_count ?? 0), 'is_featured' => (bool) $world->is_featured, 'edit_url' => route('studio.worlds.edit', ['world' => $world]), 'preview_url' => route('studio.worlds.preview', ['world' => $world]), 'public_url' => $world->publicUrl(), ]; } private function paginationMeta(LengthAwarePaginator $paginator): array { return [ 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), 'from' => $paginator->firstItem(), 'to' => $paginator->lastItem(), ]; } private function resolveSlug(string $title, World $world, array $data): string { $requested = trim(Str::slug((string) ($data['slug'] ?? ''))); $base = $requested !== '' ? $requested : Str::slug($title); $slug = $base !== '' ? $base : 'world'; $counter = 2; while (World::query() ->where('slug', $slug) ->when($world->exists, fn (Builder $builder) => $builder->where('id', '!=', $world->id)) ->exists()) { $slug = Str::limit($base !== '' ? $base : 'world', 170, '') . '-' . $counter; $counter++; } return $slug; } private function normalizePublishedAt(string $status, mixed $value): ?Carbon { if ($status === World::STATUS_PUBLISHED) { return $value ? Carbon::parse((string) $value) : now(); } return $value ? Carbon::parse((string) $value) : null; } private function normalizeSectionOrder(iterable $sectionOrder): array { $valid = array_keys((array) config('worlds.sections', [])); return collect($sectionOrder) ->map(fn ($key) => trim((string) $key)) ->filter(fn (string $key) => in_array($key, $valid, true)) ->unique() ->values() ->all(); } private function normalizeSectionVisibility(iterable $visibility): array { $valid = array_keys((array) config('worlds.sections', [])); return collect($visibility) ->mapWithKeys(fn ($value, $key): array => [(string) $key => (bool) $value]) ->filter(fn (bool $enabled, string $key): bool => in_array($key, $valid, true)) ->all(); } private function nullableText(mixed $value): ?string { $text = trim((string) ($value ?? '')); return $text === '' ? null : $text; } private function themePayload(World $world): array { $preset = (array) config('worlds.themes.' . $world->theme_key, []); return [ 'key' => $world->theme_key, 'label' => (string) ($preset['label'] ?? Str::headline((string) ($world->theme_key ?: $world->type))), 'accent_color' => $world->accent_color ?: ($preset['accent_color'] ?? '#38bdf8'), 'accent_color_secondary' => $world->accent_color_secondary ?: ($preset['accent_color_secondary'] ?? '#0f172a'), 'background_motif' => $world->background_motif ?: ($preset['background_motif'] ?? 'atmosphere'), 'icon_name' => $this->resolvedIconName($world, $preset), ]; } private function resolvedIconName(World $world, ?array $theme = null): string { $theme ??= (array) config('worlds.themes.' . $world->theme_key, []); $icon = trim((string) ($world->icon_name ?? '')); if ($icon !== '') { return $icon; } $themeIcon = trim((string) ($theme['icon_name'] ?? '')); if ($themeIcon !== '') { return $themeIcon; } return 'fa-solid fa-sparkles'; } private function timeframeLabel(World $world): ?string { if ($world->starts_at && $world->ends_at) { return $world->starts_at->format('d M Y') . ' - ' . $world->ends_at->format('d M Y'); } if ($world->starts_at) { return 'Starts ' . $world->starts_at->format('d M Y'); } if ($world->ends_at) { return 'Through ' . $world->ends_at->format('d M Y'); } return $world->edition_year ? 'Edition ' . $world->edition_year : null; } private function duplicateTitle(World $world): string { $title = trim((string) $world->title); return $title === '' ? 'World copy' : $title . ' Copy'; } private function nextEditionYear(World $world): int { return max((int) now()->year, (int) ($world->edition_year ?? now()->year)) + 1; } private function nextEditionTitle(World $world): string { $nextYear = $this->nextEditionYear($world); $title = trim((string) $world->title); if ($title === '') { return 'World ' . $nextYear; } if ($world->edition_year && str_contains($title, (string) $world->edition_year)) { return str_replace((string) $world->edition_year, (string) $nextYear, $title); } return $title . ' ' . $nextYear; } private function nextEditionSlug(World $world): string { $nextYear = $this->nextEditionYear($world); $slug = trim((string) $world->slug); if ($slug === '') { return 'world-' . $nextYear; } if ($world->edition_year && str_contains($slug, (string) $world->edition_year)) { return str_replace((string) $world->edition_year, (string) $nextYear, $slug); } return $slug . '-' . $nextYear; } private function searchArtworks(string $query): array { return Artwork::query() ->with(['user.profile', 'group', 'categories.contentType']) ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id') ->select('artworks.*') ->where('artwork_status', 'published') ->where('visibility', Artwork::VISIBILITY_PUBLIC) ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $like = '%' . $query . '%'; $nested->where('title', 'like', $like) ->orWhere('slug', 'like', $like) ->orWhere('description', 'like', $like) ->orWhereHas('user', function (Builder $userQuery) use ($like): void { $userQuery->where('username', 'like', $like) ->orWhere('name', 'like', $like); }) ->orWhereHas('group', function (Builder $groupQuery) use ($like): void { $groupQuery->where('name', 'like', $like) ->orWhere('slug', 'like', $like); }) ->orWhereHas('categories', function (Builder $categoryQuery) use ($like): void { $categoryQuery->where('name', 'like', $like) ->orWhere('slug', 'like', $like) ->orWhereHas('contentType', function (Builder $contentTypeQuery) use ($like): void { $contentTypeQuery->where('name', 'like', $like) ->orWhere('slug', 'like', $like); }); }); }); }) ->orderByRaw('COALESCE(artwork_stats.views, 0) DESC') ->orderByDesc('artworks.published_at') ->limit(8) ->get() ->map(fn (Artwork $artwork): ?array => $this->resolveArtworkPreview((int) $artwork->id, '')) ->filter() ->values() ->all(); } private function searchCollections(string $query, ?User $viewer): array { return Collection::query() ->with(['user.profile', 'coverArtwork']) ->public() ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $nested->where('title', 'like', '%' . $query . '%') ->orWhere('slug', 'like', '%' . $query . '%') ->orWhere('summary', 'like', '%' . $query . '%'); }); }) ->orderByDesc('followers_count') ->limit(8) ->get() ->map(fn (Collection $collection): ?array => $this->resolveCollectionPreview((int) $collection->id, $viewer, '')) ->filter() ->values() ->all(); } private function searchUsers(string $query): array { return User::query() ->with(['profile', 'statistics']) ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $nested->where('username', 'like', '%' . $query . '%') ->orWhere('name', 'like', '%' . $query . '%'); }); }) ->orderByDesc('nova_featured_creator') ->orderBy('username') ->limit(8) ->get() ->map(fn (User $user): ?array => $this->resolveUserPreview((int) $user->id, '')) ->filter() ->values() ->all(); } private function searchGroups(string $query, ?User $viewer): array { return Group::query() ->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges']) ->public() ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $nested->where('name', 'like', '%' . $query . '%') ->orWhere('slug', 'like', '%' . $query . '%') ->orWhere('headline', 'like', '%' . $query . '%'); }); }) ->orderByDesc('followers_count') ->limit(8) ->get() ->map(fn (Group $group): ?array => $this->resolveGroupPreview((int) $group->id, $viewer, '')) ->filter() ->values() ->all(); } private function searchNews(string $query): array { return NewsArticle::query() ->with(['author.profile', 'category']) ->published() ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $nested->where('title', 'like', '%' . $query . '%') ->orWhere('slug', 'like', '%' . $query . '%') ->orWhere('excerpt', 'like', '%' . $query . '%'); }); }) ->orderByDesc('published_at') ->limit(8) ->get() ->map(fn (NewsArticle $article): ?array => $this->resolveNewsPreview((int) $article->id, '')) ->filter() ->values() ->all(); } private function searchChallenges(string $query, ?User $viewer): array { return GroupChallenge::query() ->with('group') ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%')) ->orderByDesc('start_at') ->limit(8) ->get() ->map(fn (GroupChallenge $challenge): ?array => $this->resolveChallengePreview((int) $challenge->id, $viewer, '')) ->filter() ->values() ->all(); } private function searchEvents(string $query, ?User $viewer): array { return GroupEvent::query() ->with('group') ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%')) ->orderByDesc('start_at') ->limit(8) ->get() ->map(fn (GroupEvent $event): ?array => $this->resolveEventPreview((int) $event->id, $viewer, '')) ->filter() ->values() ->all(); } private function searchReleases(string $query, ?User $viewer): array { return GroupRelease::query() ->with('group') ->when($query !== '', fn (Builder $builder): Builder => $builder->where('title', 'like', '%' . $query . '%')) ->orderByDesc('published_at') ->limit(8) ->get() ->map(fn (GroupRelease $release): ?array => $this->resolveReleasePreview((int) $release->id, $viewer, '')) ->filter() ->values() ->all(); } private function searchCards(string $query, ?User $viewer): array { return NovaCard::query() ->with(['user.profile', 'category']) ->when($query !== '', function (Builder $builder) use ($query): void { $builder->where(function (Builder $nested) use ($query): void { $nested->where('title', 'like', '%' . $query . '%') ->orWhere('slug', 'like', '%' . $query . '%') ->orWhere('description', 'like', '%' . $query . '%'); }); }) ->orderByDesc('published_at') ->limit(8) ->get() ->map(fn (NovaCard $card): ?array => $this->resolveCardPreview((int) $card->id, $viewer, '')) ->filter() ->values() ->all(); } public function resolveEntityPreview(string $type, int $entityId, ?User $viewer = null, string $contextLabel = ''): ?array { return match ($type) { WorldRelation::TYPE_ARTWORK => $this->resolveArtworkPreview($entityId, $contextLabel), WorldRelation::TYPE_COLLECTION => $this->resolveCollectionPreview($entityId, $viewer, $contextLabel), WorldRelation::TYPE_USER => $this->resolveUserPreview($entityId, $contextLabel), WorldRelation::TYPE_GROUP => $this->resolveGroupPreview($entityId, $viewer, $contextLabel), WorldRelation::TYPE_NEWS => $this->resolveNewsPreview($entityId, $contextLabel), WorldRelation::TYPE_CHALLENGE => $this->resolveChallengePreview($entityId, $viewer, $contextLabel), WorldRelation::TYPE_EVENT => $this->resolveEventPreview($entityId, $viewer, $contextLabel), WorldRelation::TYPE_RELEASE => $this->resolveReleasePreview($entityId, $viewer, $contextLabel), WorldRelation::TYPE_CARD => $this->resolveCardPreview($entityId, $viewer, $contextLabel), default => null, }; } private function resolveArtworkPreview(int $entityId, string $contextLabel): ?array { $artwork = Artwork::query()->with(['user.profile', 'categories.contentType', 'stats'])->find($entityId); if (! $artwork || (string) $artwork->artwork_status !== 'published' || (string) $artwork->visibility !== Artwork::VISIBILITY_PUBLIC) { return null; } $resource = ArtworkListResource::make($artwork)->toArray(request()); $views = (int) ($artwork->stats?->views ?? 0); return [ 'id' => (int) $artwork->id, 'entity_type' => WorldRelation::TYPE_ARTWORK, 'entity_label' => (string) (config('worlds.relation_types.artwork') ?? 'Artwork'), 'title' => (string) ($resource['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'), 'avatar' => null, 'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured artwork', 'meta' => array_values(array_filter([ $resource['category']['name'] ?? null, $views > 0 ? number_format($views) . ' views' : null, ])), ]; } private function resolveCollectionPreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $collection = Collection::query()->with(['user.profile', 'group', 'coverArtwork'])->find($entityId); if (! $collection || ! $collection->canBeViewedBy($viewer) || ! $collection->user?->username) { return null; } $payload = $this->collections->mapCollectionCardPayloads([$collection], false, $viewer)[0] ?? null; if (! $payload) { return null; } return array_merge($payload, [ 'entity_type' => WorldRelation::TYPE_COLLECTION, 'entity_label' => (string) (config('worlds.relation_types.collection') ?? 'Collection'), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Curated collection', ]); } private function resolveUserPreview(int $entityId, string $contextLabel): ?array { $user = User::query()->with(['profile', 'statistics'])->find($entityId); if (! $user || ! $user->username) { return null; } return [ 'id' => (int) $user->id, 'entity_type' => WorldRelation::TYPE_USER, 'entity_label' => (string) (config('worlds.relation_types.user') ?? 'Creator'), 'title' => (string) ($user->name ?: $user->username), 'subtitle' => $user->username ? '@' . $user->username : null, 'description' => Str::limit((string) ($user->profile?->description ?? $user->profile?->about ?? ''), 140), 'url' => route('profile.show', ['username' => strtolower((string) $user->username)]), 'image' => $user->cover_hash && $user->cover_ext ? CoverUrl::forUser($user->cover_hash, $user->cover_ext, $user->updated_at?->timestamp) : null, 'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 128), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured creator', 'meta' => array_values(array_filter([ $user->nova_featured_creator ? 'Featured creator' : null, (int) $user->followers_count > 0 ? number_format((int) $user->followers_count) . ' followers' : null, ])), ]; } private function resolveGroupPreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $group = Group::query()->with(['owner.profile', 'recruitmentProfile', 'discoveryMetric', 'members', 'badges'])->find($entityId); if (! $group || ! $group->canBeViewedBy($viewer)) { return null; } return array_merge($this->groups->mapGroupCard($group, $viewer), [ 'entity_type' => WorldRelation::TYPE_GROUP, 'entity_label' => (string) (config('worlds.relation_types.group') ?? 'Group'), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Featured group', ]); } private function resolveNewsPreview(int $entityId, string $contextLabel): ?array { $article = NewsArticle::query()->with(['author.profile', 'category'])->published()->find($entityId); if (! $article) { return null; } return [ 'id' => (int) $article->id, 'entity_type' => WorldRelation::TYPE_NEWS, 'entity_label' => (string) (config('worlds.relation_types.news') ?? 'News article'), 'title' => (string) $article->title, 'subtitle' => (string) ($article->category?->name ?? 'News'), 'description' => Str::limit((string) ($article->excerpt ?: strip_tags((string) $article->content)), 160), 'url' => route('news.show', ['slug' => $article->slug]), 'image' => $article->cover_url, 'avatar' => $article->author ? AvatarUrl::forUser((int) $article->author->id, $article->author->profile?->avatar_hash, 72) : null, 'context_label' => $contextLabel !== '' ? $contextLabel : 'Related story', 'meta' => array_values(array_filter([ $article->published_at?->format('d M Y'), (string) ($article->type_label ?? ''), ])), ]; } private function resolveChallengePreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $challenge = GroupChallenge::query()->with('group')->find($entityId); if (! $challenge || ! $challenge->group || ! $challenge->canBeViewedBy($viewer)) { return null; } return [ 'id' => (int) $challenge->id, 'entity_type' => WorldRelation::TYPE_CHALLENGE, 'entity_label' => (string) (config('worlds.relation_types.challenge') ?? 'Challenge'), 'title' => (string) $challenge->title, 'subtitle' => (string) $challenge->group->name, 'description' => Str::limit((string) ($challenge->summary ?: $challenge->description ?: ''), 140), 'url' => route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]), 'image' => $challenge->coverUrl(), 'avatar' => $challenge->group->avatarUrl(), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Join the challenge', 'meta' => array_values(array_filter([ $challenge->start_at?->format('d M Y'), Str::headline((string) $challenge->status), ])), ]; } private function resolveEventPreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $event = GroupEvent::query()->with('group')->find($entityId); if (! $event || ! $event->group || ! $event->canBeViewedBy($viewer)) { return null; } return [ 'id' => (int) $event->id, 'entity_type' => WorldRelation::TYPE_EVENT, 'entity_label' => (string) (config('worlds.relation_types.event') ?? 'Event'), 'title' => (string) $event->title, 'subtitle' => (string) $event->group->name, 'description' => Str::limit((string) ($event->summary ?: $event->description ?: ''), 140), 'url' => route('groups.events.show', ['group' => $event->group, 'event' => $event]), 'image' => $event->coverUrl(), 'avatar' => $event->group->avatarUrl(), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Related event', 'meta' => array_values(array_filter([ $event->start_at?->format('d M Y H:i'), $event->location, ])), ]; } private function resolveReleasePreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $release = GroupRelease::query()->with('group')->find($entityId); if (! $release || ! $release->group || ! $release->canBeViewedBy($viewer)) { return null; } return [ 'id' => (int) $release->id, 'entity_type' => WorldRelation::TYPE_RELEASE, 'entity_label' => (string) (config('worlds.relation_types.release') ?? 'Release'), 'title' => (string) $release->title, 'subtitle' => (string) $release->group->name, 'description' => Str::limit((string) ($release->summary ?: $release->description ?: ''), 140), 'url' => route('groups.releases.show', ['group' => $release->group, 'release' => $release]), 'image' => $release->coverUrl(), 'avatar' => $release->group->avatarUrl(), 'context_label' => $contextLabel !== '' ? $contextLabel : 'Release spotlight', 'meta' => array_values(array_filter([ $release->published_at?->format('d M Y'), Str::headline((string) $release->status), ])), ]; } private function resolveCardPreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $card = NovaCard::query()->with(['user.profile', 'category'])->find($entityId); if (! $card || ! $card->canBeViewedBy($viewer)) { return null; } return [ 'id' => (int) $card->id, 'entity_type' => WorldRelation::TYPE_CARD, 'entity_label' => (string) (config('worlds.relation_types.card') ?? 'Card'), 'title' => (string) ($card->title ?: 'Untitled card'), 'subtitle' => (string) ($card->category?->name ?? ''), 'description' => Str::limit((string) ($card->description ?? $card->quote_text ?? ''), 140), 'url' => $card->publicUrl(), 'image' => $card->previewUrl(), 'avatar' => $card->user ? AvatarUrl::forUser((int) $card->user->id, $card->user->profile?->avatar_hash, 72) : null, 'context_label' => $contextLabel !== '' ? $contextLabel : 'Themed card', 'meta' => array_values(array_filter([ Str::headline((string) $card->format), (int) $card->likes_count > 0 ? number_format((int) $card->likes_count) . ' likes' : null, ])), ]; } private function entityMapKey(string $type, int $id): string { return $type . ':' . $id; } private function deleteWorldMediaIfReplaced(string $originalPath, string $currentPath): void { $trimmedOriginal = ltrim(trim($originalPath), '/'); $trimmedCurrent = ltrim(trim($currentPath), '/'); if ($trimmedOriginal === '' || $trimmedOriginal === $trimmedCurrent || ! Str::startsWith($trimmedOriginal, 'worlds/media/')) { return; } Storage::disk((string) config('covers.disk', 's3'))->delete($trimmedOriginal); } }