where('slug', $slug) ->when($ignoreGroupId !== null, fn ($query) => $query->where('id', '!=', $ignoreGroupId)) ->exists()) { $slug = Str::limit($base, 84, '') . '-' . $suffix; $suffix++; } return $slug; } public function createGroup(User $owner, array $attributes): Group { $storedAvatarPath = null; $storedBannerPath = null; try { return DB::transaction(function () use ($owner, $attributes, &$storedAvatarPath, &$storedBannerPath): Group { $group = new Group(); $group->owner()->associate($owner); $group->featured_artwork_id = null; $group->is_verified = false; $group->founded_at = $attributes['founded_at'] ?? null; $group->name = (string) $attributes['name']; $group->slug = $this->makeUniqueSlug((string) ($attributes['slug'] ?? $attributes['name'])); $group->headline = $attributes['headline'] ?? null; $group->bio = $attributes['bio'] ?? null; $group->type = $attributes['type'] ?? null; $group->visibility = (string) ($attributes['visibility'] ?? Group::VISIBILITY_PUBLIC); $group->status = Group::LIFECYCLE_ACTIVE; $group->membership_policy = (string) ($attributes['membership_policy'] ?? Group::MEMBERSHIP_INVITE_ONLY); $group->website_url = $attributes['website_url'] ?? null; $group->links_json = $this->normalizeLinks($attributes['links_json'] ?? []); $group->avatar_path = $this->normalizeMediaPath($attributes['avatar_path'] ?? null); $group->banner_path = $this->normalizeMediaPath($attributes['banner_path'] ?? null); $group->artworks_count = 0; $group->collections_count = 0; $group->followers_count = 0; $group->last_activity_at = now(); $group->save(); if (($attributes['avatar_file'] ?? null) instanceof UploadedFile) { $storedAvatarPath = $this->media->storeUploadedImage($group, $attributes['avatar_file'], 'avatar'); $group->avatar_path = $storedAvatarPath; } if (($attributes['banner_file'] ?? null) instanceof UploadedFile) { $storedBannerPath = $this->media->storeUploadedImage($group, $attributes['banner_file'], 'banner'); $group->banner_path = $storedBannerPath; } if ($storedAvatarPath !== null || $storedBannerPath !== null) { $group->save(); } $this->memberships->ensureOwnerMembership($group); return $group->fresh(['owner.profile']); }); } catch (\Throwable $exception) { $this->media->deleteIfManaged($storedAvatarPath); $this->media->deleteIfManaged($storedBannerPath); throw $exception; } } public function updateGroup(Group $group, array $attributes, User $actor): Group { $storedAvatarPath = null; $storedBannerPath = null; $obsoleteAvatarPath = null; $obsoleteBannerPath = null; try { $updatedGroup = DB::transaction(function () use ($group, $attributes, $actor, &$storedAvatarPath, &$storedBannerPath, &$obsoleteAvatarPath, &$obsoleteBannerPath): Group { $originalAvatarPath = $group->avatar_path; $originalBannerPath = $group->banner_path; $group->fill([ 'name' => (string) ($attributes['name'] ?? $group->name), 'slug' => $this->makeUniqueSlug((string) ($attributes['slug'] ?? $attributes['name'] ?? $group->slug), (int) $group->id), 'headline' => $attributes['headline'] ?? null, 'bio' => $attributes['bio'] ?? null, 'type' => $attributes['type'] ?? $group->type, 'visibility' => (string) ($attributes['visibility'] ?? $group->visibility), 'membership_policy' => (string) ($attributes['membership_policy'] ?? $group->membership_policy ?? Group::MEMBERSHIP_INVITE_ONLY), 'founded_at' => $attributes['founded_at'] ?? $group->founded_at, 'website_url' => $attributes['website_url'] ?? null, 'links_json' => $this->normalizeLinks($attributes['links_json'] ?? $group->links_json ?? []), 'avatar_path' => array_key_exists('avatar_path', $attributes) ? $this->normalizeMediaPath($attributes['avatar_path']) : $group->avatar_path, 'banner_path' => array_key_exists('banner_path', $attributes) ? $this->normalizeMediaPath($attributes['banner_path']) : $group->banner_path, 'featured_artwork_id' => $this->normalizeFeaturedArtworkId($group, $attributes['featured_artwork_id'] ?? $group->featured_artwork_id), 'last_activity_at' => now(), ]); $group->save(); if (($attributes['avatar_file'] ?? null) instanceof UploadedFile) { $storedAvatarPath = $this->media->storeUploadedImage($group, $attributes['avatar_file'], 'avatar'); $group->avatar_path = $storedAvatarPath; } if (($attributes['banner_file'] ?? null) instanceof UploadedFile) { $storedBannerPath = $this->media->storeUploadedImage($group, $attributes['banner_file'], 'banner'); $group->banner_path = $storedBannerPath; } if ($storedAvatarPath !== null || $storedBannerPath !== null) { $group->save(); } $this->memberships->ensureOwnerMembership($group); $obsoleteAvatarPath = $originalAvatarPath !== $group->avatar_path ? $originalAvatarPath : null; $obsoleteBannerPath = $originalBannerPath !== $group->banner_path ? $originalBannerPath : null; return $group->fresh(['owner.profile']); }); } catch (\Throwable $exception) { $this->media->deleteIfManaged($storedAvatarPath); $this->media->deleteIfManaged($storedBannerPath); throw $exception; } $this->media->deleteIfManaged($obsoleteAvatarPath); $this->media->deleteIfManaged($obsoleteBannerPath); return $updatedGroup; } public function syncArtworkCount(Group $group): void { $group->forceFill([ 'artworks_count' => Artwork::query() ->where('group_id', $group->id) ->whereNull('deleted_at') ->count(), 'last_activity_at' => now(), ])->save(); } public function syncCollectionCount(Group $group): void { $group->forceFill([ 'collections_count' => Collection::query() ->where('group_id', $group->id) ->whereNull('deleted_at') ->count(), 'last_activity_at' => now(), ])->save(); } public function studioOptionsForUser(User $user): array { $groups = Group::query() ->with(['owner.profile', 'members']) ->where('status', '!=', Group::LIFECYCLE_SUSPENDED) ->where(function ($query) use ($user): void { $query->where('owner_user_id', $user->id) ->orWhereHas('members', function ($memberQuery) use ($user): void { $memberQuery->where('user_id', $user->id) ->where('status', Group::STATUS_ACTIVE); }); }) ->orderBy('name') ->get(); return $groups->map(function (Group $group) use ($user): array { $canPublishArtworks = $group->canPublishArtworks($user); $canSubmitArtworkForReview = $group->canSubmitArtworkForReview($user); $canManageReleases = $group->canManageReleases($user); $canViewReputation = $group->canViewReputationDashboard($user); return [ 'id' => (int) $group->id, 'name' => (string) $group->name, 'slug' => (string) $group->slug, 'role' => $group->activeRoleFor($user), 'role_label' => Group::displayRole($group->activeRoleFor($user)), 'status' => (string) ($group->status ?? Group::LIFECYCLE_ACTIVE), 'avatar_url' => $group->avatarUrl(), 'artworks_count' => (int) $group->artworks_count, 'collections_count' => (int) $group->collections_count, 'followers_count' => (int) $group->followers_count, 'permissions' => [ 'can_publish_artworks' => $canPublishArtworks, 'can_submit_artwork_for_review' => $canSubmitArtworkForReview, ], 'public_url' => $group->publicUrl(), 'studio_url' => route('studio.groups.show', ['group' => $group]), 'studio_artworks_url' => route('studio.groups.artworks', ['group' => $group]), 'studio_collections_url' => route('studio.groups.collections', ['group' => $group]), 'studio_members_url' => route('studio.groups.members', ['group' => $group]), 'studio_invitations_url' => route('studio.groups.invitations', ['group' => $group]), 'studio_join_requests_url' => route('studio.groups.join-requests', ['group' => $group]), 'studio_review_url' => route('studio.groups.review', ['group' => $group]), 'studio_recruitment_url' => route('studio.groups.recruitment', ['group' => $group]), 'studio_posts_url' => route('studio.groups.posts.index', ['group' => $group]), 'studio_settings_url' => route('studio.groups.settings', ['group' => $group]), 'studio_projects_url' => route('studio.groups.projects.index', ['group' => $group]), 'studio_releases_url' => $canManageReleases ? route('studio.groups.releases.index', ['group' => $group]) : null, 'studio_challenges_url' => route('studio.groups.challenges.index', ['group' => $group]), 'studio_events_url' => route('studio.groups.events.index', ['group' => $group]), 'studio_assets_url' => route('studio.groups.assets.index', ['group' => $group]), 'studio_reputation_url' => $canViewReputation ? route('studio.groups.reputation', ['group' => $group]) : null, 'studio_activity_url' => route('studio.groups.activity', ['group' => $group]), 'upload_url' => ($canPublishArtworks || $canSubmitArtworkForReview) ? route('upload', ['group' => $group->slug]) : null, 'collection_create_url' => route('settings.collections.create', ['group' => $group->slug]), ]; })->values()->all(); } public function mapGroupCard(Group $group, ?User $viewer = null): array { return $this->cards->mapGroupCard($group, $viewer); } public function mapGroupDetail(Group $group, ?User $viewer = null): array { $recruitment = $this->recruitment->payloadForGroup($group); return array_merge($this->mapGroupCard($group, $viewer), [ 'website_url' => $group->website_url, 'bio' => $group->bio, 'links' => $this->normalizeLinks($group->links_json ?? []), 'avatar_path' => $group->avatar_path, 'banner_path' => $group->banner_path, 'featured_artwork_id' => $group->featured_artwork_id ? (int) $group->featured_artwork_id : null, 'founded_at' => $group->founded_at?->toISOString(), 'last_activity_at' => $group->last_activity_at?->toISOString(), 'created_at' => $group->created_at?->toISOString(), 'current_join_request' => $this->joinRequests->currentRequestFor($group, $viewer), 'recruitment' => $recruitment, 'pinned_post' => $this->posts->pinnedPost($group), 'featured_release' => $this->releases->featuredRelease($group, $viewer), 'featured_project' => $this->projects->featuredProject($group, $viewer), 'active_challenge' => $this->challenges->activeChallenge($group, $viewer), 'upcoming_event' => $this->events->upcomingEvent($group, $viewer), 'badge_showcase' => $this->reputation->groupBadges($group, 8), 'top_contributors' => $this->reputation->topContributors($group, 6), 'trust_signals' => $this->reputation->trustSignals($group), ]); } public function recentPostCards(Group $group, int $limit = 3): array { return $this->posts->recentPosts($group, $limit); } public function recentProjectCards(Group $group, ?User $viewer = null, int $limit = 3): array { return $this->projects->publicListing($group, $viewer, $limit); } public function recentReleaseCards(Group $group, ?User $viewer = null, int $limit = 3): array { return $this->releases->publicListing($group, $viewer, $limit); } public function recentChallengeCards(Group $group, ?User $viewer = null, int $limit = 3): array { return $this->challenges->publicListing($group, $viewer, $limit); } public function recentEventCards(Group $group, ?User $viewer = null, int $limit = 3): array { return $this->events->publicListing($group, $viewer, $limit); } public function publicProjectListing(Group $group, ?User $viewer = null, int $limit = 12): array { return $this->projects->publicListing($group, $viewer, $limit); } public function publicReleaseListing(Group $group, ?User $viewer = null, int $limit = 12): array { return $this->releases->publicListing($group, $viewer, $limit); } public function publicChallengeListing(Group $group, ?User $viewer = null, int $limit = 12): array { return $this->challenges->publicListing($group, $viewer, $limit); } public function publicEventListing(Group $group, ?User $viewer = null, int $limit = 12): array { return $this->events->publicListing($group, $viewer, $limit); } public function publicAssetListing(Group $group, int $limit = 12): array { return $this->assets->publicListing($group, $limit); } public function publicActivityFeed(Group $group, int $limit = 8): array { return $this->activity->publicFeed($group, $limit); } public function studioActivityFeed(Group $group, User $viewer, int $limit = 20): array { return $this->activity->studioFeed($group, $viewer, $limit); } public function publicPostListing(Group $group, int $limit = 12): array { return $this->posts->publicPosts($group, $limit); } public function recruitmentPayload(Group $group): ?array { return $this->recruitment->payloadForGroup($group); } public function recentHistory(Group $group, int $limit = 8): array { return $this->history->recentFor($group, $limit); } public function featuredArtworkCards(Group $group, int $limit = 4): array { $query = Artwork::query() ->with(['user.profile', 'group', 'primaryAuthor.profile']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->where('is_public', true) ->where('is_approved', true) ->whereNotNull('published_at'); if ((int) ($group->featured_artwork_id ?? 0) > 0) { $featuredArtwork = (clone $query) ->where('id', (int) $group->featured_artwork_id) ->first(); $remaining = (clone $query) ->where('id', '!=', (int) $group->featured_artwork_id) ->latest('published_at') ->limit(max($limit - ($featuredArtwork ? 1 : 0), 0)) ->get(); return collect([$featuredArtwork]) ->filter() ->concat($remaining) ->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork)) ->values() ->all(); } return (clone $query) ->latest('published_at') ->limit($limit) ->get() ->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork)) ->values() ->all(); } public function featuredCollectionCards(Group $group, ?User $viewer = null, int $limit = 3): array { $collections = Collection::query() ->with(['user.profile', 'group', 'coverArtwork']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->where('is_featured', true) ->latest('featured_at') ->latest('updated_at') ->limit($limit) ->get() ->filter(fn (Collection $collection): bool => $collection->isPubliclyAccessible()) ->values(); return $this->collections->mapCollectionCardPayloads($collections, false, $viewer); } public function mapLeadershipPreview(array $members, array $owner, int $limit = 4): array { $leadership = collect($members) ->filter(fn (array $member): bool => in_array((string) ($member['role'] ?? ''), [Group::ROLE_OWNER, Group::ROLE_ADMIN], true)) ->map(function (array $member): array { return [ 'id' => (int) ($member['user']['id'] ?? 0), 'name' => $member['user']['name'] ?? null, 'username' => $member['user']['username'] ?? null, 'avatar_url' => $member['user']['avatar_url'] ?? null, 'profile_url' => $member['user']['profile_url'] ?? null, 'role' => (string) ($member['role'] ?? ''), 'role_label' => $member['role_label'] ?? Group::displayRole((string) ($member['role'] ?? '')), ]; }) ->unique('id') ->values(); if ($leadership->isEmpty() && ! empty($owner['id'])) { $leadership = collect([array_merge($owner, [ 'role' => Group::ROLE_OWNER, 'role_label' => Group::displayRole(Group::ROLE_OWNER), ])]); } return $leadership->take($limit)->all(); } public function archiveGroup(Group $group, User $actor): Group { if (! $group->canArchive($actor) && ! $actor->isAdmin()) { abort(403); } $group->forceFill([ 'status' => Group::LIFECYCLE_ARCHIVED, 'last_activity_at' => now(), ])->save(); return $group->fresh(['owner.profile', 'members']); } public function publicArtworkCards(Group $group, int $limit = 18): array { return Artwork::query() ->with(['user.profile', 'group', 'primaryAuthor.profile']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->where('is_public', true) ->where('is_approved', true) ->whereNotNull('published_at') ->latest('published_at') ->limit($limit) ->get() ->map(fn (Artwork $artwork): array => $this->mapPublicArtworkCard($artwork)) ->values() ->all(); } public function publicCollectionCards(Group $group, ?User $viewer = null, int $limit = 12): array { $collections = Collection::query() ->with(['user.profile', 'group', 'coverArtwork']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->latest('updated_at') ->limit($limit) ->get() ->filter(fn (Collection $collection): bool => $collection->isPubliclyAccessible()) ->values(); return $this->collections->mapCollectionCardPayloads($collections, false, $viewer); } private function mapPublicArtworkCard(Artwork $artwork): array { return [ 'id' => (int) $artwork->id, 'title' => (string) $artwork->title, 'url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]), 'thumb' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'), 'thumb_srcset' => ThumbnailPresenter::srcsetForArtwork($artwork), 'author' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username ?: 'Artist', 'published_at' => $artwork->published_at?->toISOString(), ]; } public function studioDashboardSummary(Group $group): array { $artworkQuery = Artwork::query() ->where('group_id', $group->id) ->whereNull('deleted_at'); $collectionQuery = Collection::query() ->where('group_id', $group->id) ->whereNull('deleted_at'); return [ 'draft_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'draft')->count(), 'scheduled_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'scheduled')->count(), 'published_artworks_count' => (clone $artworkQuery)->where('artwork_status', 'published')->count(), 'pending_reviews_count' => (clone $artworkQuery)->where('group_review_status', 'submitted')->count(), 'draft_collections_count' => (clone $collectionQuery) ->where(function ($builder): void { $builder->where('lifecycle_state', Collection::LIFECYCLE_DRAFT) ->orWhere('workflow_state', Collection::WORKFLOW_DRAFT) ->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW); }) ->count(), 'active_members_count' => (int) $group->members()->where('status', Group::STATUS_ACTIVE)->count(), 'pending_invites_count' => $this->memberships->pendingInviteCount($group), 'pending_join_requests_count' => $this->joinRequests->pendingCount($group), 'published_posts_count' => (int) $group->posts()->where('status', \App\Models\GroupPost::STATUS_PUBLISHED)->count(), 'is_recruiting' => (bool) ($this->recruitment->payloadForGroup($group)['is_recruiting'] ?? false), 'projects_count' => (int) $group->projects()->count(), 'releases_count' => (int) $group->releases()->count(), 'published_releases_count' => (int) $group->releases()->where('status', \App\Models\GroupRelease::STATUS_RELEASED)->count(), 'active_challenges_count' => (int) $group->challenges()->whereIn('status', ['published', 'active'])->count(), 'events_count' => (int) $group->events()->count(), 'assets_count' => (int) $group->assets()->count(), 'activity_count' => (int) $group->activityItems()->count(), 'group_badges_count' => (int) $group->badges()->count(), 'member_badges_count' => (int) $group->memberBadges()->count(), 'trust_score' => (float) ($group->discoveryMetric?->trust_score ?? 0), ]; } public function studioArtworkPreviewItems(Group $group, string $bucket = 'all', int $limit = 6): array { $query = Artwork::query() ->with(['user.profile', 'primaryAuthor.profile', 'stats']) ->where('group_id', $group->id) ->whereNull('deleted_at'); if ($bucket === 'drafts') { $query->where('artwork_status', 'draft'); } elseif ($bucket === 'scheduled') { $query->where('artwork_status', 'scheduled'); } elseif ($bucket === 'published') { $query->where('artwork_status', 'published'); } return $query->latest('updated_at') ->limit($limit) ->get() ->map(fn (Artwork $artwork): array => $this->mapStudioArtworkItem($artwork)) ->values() ->all(); } public function studioFeaturedArtworkOptions(Group $group, int $limit = 24): array { return Artwork::query() ->with(['user.profile', 'primaryAuthor.profile']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->where('artwork_status', 'published') ->where('is_public', true) ->where('is_approved', true) ->whereNotNull('published_at') ->latest('published_at') ->limit($limit) ->get() ->map(fn (Artwork $artwork): array => [ 'id' => (int) $artwork->id, 'title' => (string) $artwork->title, 'author' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username, 'thumb' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'), 'published_at' => $artwork->published_at?->toISOString(), ]) ->values() ->all(); } public function studioCollectionPreviewItems(Group $group, int $limit = 6): array { return Collection::query() ->with(['user.profile', 'group', 'coverArtwork']) ->where('group_id', $group->id) ->whereNull('deleted_at') ->latest('updated_at') ->limit($limit) ->get() ->map(fn (Collection $collection): array => $this->mapStudioCollectionItem($collection)) ->values() ->all(); } public function studioArtworkListing(Group $group, array $filters = []): array { $bucket = (string) ($filters['bucket'] ?? 'all'); $search = trim((string) ($filters['q'] ?? '')); $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48); $query = Artwork::query() ->with(['user.profile', 'primaryAuthor.profile']) ->where('group_id', $group->id) ->whereNull('deleted_at'); if ($bucket === 'drafts') { $query->where('artwork_status', 'draft'); } elseif ($bucket === 'scheduled') { $query->where('artwork_status', 'scheduled'); } elseif ($bucket === 'published') { $query->where('artwork_status', 'published'); } if ($search !== '') { $query->where(function ($builder) use ($search): void { $builder->where('title', 'like', '%' . $search . '%') ->orWhere('description', 'like', '%' . $search . '%'); }); } $paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page); return $this->mapStudioListing( $paginator, fn (Artwork $artwork): array => $this->mapStudioArtworkItem($artwork), [ ['value' => 'all', 'label' => 'All'], ['value' => 'published', 'label' => 'Published'], ['value' => 'drafts', 'label' => 'Drafts'], ['value' => 'scheduled', 'label' => 'Scheduled'], ], $bucket, $search ); } public function studioCollectionListing(Group $group, array $filters = []): array { $bucket = (string) ($filters['bucket'] ?? 'all'); $search = trim((string) ($filters['q'] ?? '')); $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48); $query = Collection::query() ->with(['user.profile', 'group', 'coverArtwork']) ->where('group_id', $group->id) ->whereNull('deleted_at'); if ($bucket === 'drafts') { $query->where(function ($builder): void { $builder->where('lifecycle_state', Collection::LIFECYCLE_DRAFT) ->orWhere('workflow_state', Collection::WORKFLOW_DRAFT) ->orWhere('workflow_state', Collection::WORKFLOW_IN_REVIEW); }); } elseif ($bucket === 'scheduled') { $query->where('lifecycle_state', Collection::LIFECYCLE_SCHEDULED); } elseif ($bucket === 'published') { $query->whereIn('lifecycle_state', [Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_SCHEDULED]); } if ($search !== '') { $query->where(function ($builder) use ($search): void { $builder->where('title', 'like', '%' . $search . '%') ->orWhere('description', 'like', '%' . $search . '%'); }); } $paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page); return $this->mapStudioListing( $paginator, fn (Collection $collection): array => $this->mapStudioCollectionItem($collection), [ ['value' => 'all', 'label' => 'All'], ['value' => 'published', 'label' => 'Published'], ['value' => 'drafts', 'label' => 'Drafts'], ['value' => 'scheduled', 'label' => 'Scheduled'], ], $bucket, $search ); } private function mapStudioListing(LengthAwarePaginator $paginator, callable $mapper, array $bucketOptions, string $bucket, string $search): array { return [ 'items' => collect($paginator->items())->map($mapper)->values()->all(), 'meta' => [ 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), ], 'filters' => [ 'bucket' => $bucket, 'q' => $search, 'sort' => 'updated_desc', ], 'module_options' => [], 'bucket_options' => $bucketOptions, 'sort_options' => [ ['value' => 'updated_desc', 'label' => 'Recently updated'], ], 'advanced_filters' => [], 'default_view' => 'grid', ]; } private function mapStudioArtworkItem(Artwork $artwork): array { $status = (string) ($artwork->artwork_status ?: ($artwork->published_at ? 'published' : 'draft')); return [ 'id' => 'artworks:' . (int) $artwork->id, 'numeric_id' => (int) $artwork->id, 'module' => 'artworks', 'module_label' => 'Artworks', 'module_icon' => 'fa-solid fa-images', 'title' => (string) $artwork->title, 'subtitle' => $artwork->primaryAuthor?->name ?: $artwork->primaryAuthor?->username ?: $artwork->user?->name ?: $artwork->user?->username, 'description' => $artwork->description, 'status' => $status, 'visibility' => (string) ($artwork->visibility ?: Artwork::VISIBILITY_PRIVATE), 'image_url' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? $artwork->thumbUrl('md'), 'preview_url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]), 'view_url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: $artwork->id]), 'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]), 'manage_url' => route('studio.artworks.edit', ['id' => $artwork->id]), 'analytics_url' => route('studio.artworks.analytics', ['id' => $artwork->id]), 'created_at' => $artwork->created_at?->toISOString(), 'updated_at' => $artwork->updated_at?->toISOString(), 'published_at' => $artwork->published_at?->toISOString(), 'metrics' => [ 'views' => (int) ($artwork->stats?->views ?? 0), 'appreciation' => (int) ($artwork->stats?->favorites ?? 0), 'comments' => (int) $artwork->comments()->count(), ], 'actions' => [], ]; } private function mapStudioCollectionItem(Collection $collection): array { $mapped = $this->collections->mapCollectionCardPayloads([$collection->loadMissing(['user.profile', 'group', 'coverArtwork'])], true)[0]; $status = $mapped['lifecycle_state'] === Collection::LIFECYCLE_FEATURED ? 'published' : ($mapped['lifecycle_state'] ?? 'draft'); return [ 'id' => 'collections:' . (int) $collection->id, 'numeric_id' => (int) $collection->id, 'module' => 'collections', 'module_label' => 'Collections', 'module_icon' => 'fa-solid fa-layer-group', 'title' => (string) $mapped['title'], 'subtitle' => $mapped['subtitle'] ?: ucfirst((string) ($mapped['type'] ?? 'collection')), 'description' => $mapped['summary'] ?: $mapped['description'], 'status' => $status, 'visibility' => (string) $mapped['visibility'], 'image_url' => $mapped['cover_image'], 'preview_url' => $mapped['url'], 'view_url' => $mapped['url'], 'edit_url' => $mapped['edit_url'] ?: $mapped['manage_url'], 'manage_url' => $mapped['manage_url'], 'analytics_url' => route('settings.collections.analytics', ['collection' => $collection->id]), 'created_at' => ($mapped['published_at'] ?? null) ?: ($mapped['updated_at'] ?? null), 'updated_at' => $mapped['updated_at'] ?? null, 'published_at' => $mapped['published_at'] ?? null, 'metrics' => [ 'views' => (int) ($mapped['views_count'] ?? 0), 'appreciation' => (int) (($mapped['likes_count'] ?? 0) + ($mapped['followers_count'] ?? 0)), 'comments' => (int) ($mapped['comments_count'] ?? 0), ], 'actions' => [], ]; } private function normalizeLinks(mixed $links): array { $items = is_array($links) ? $links : []; return collect($items) ->filter(fn ($item): bool => is_array($item)) ->map(function (array $item): array { return [ 'label' => trim((string) ($item['label'] ?? '')), 'url' => trim((string) ($item['url'] ?? '')), ]; }) ->filter(fn (array $item): bool => $item['label'] !== '' && $item['url'] !== '') ->values() ->all(); } private function normalizeMediaPath(mixed $path): ?string { $trimmed = trim((string) $path); return $trimmed !== '' ? $trimmed : null; } private function normalizeFeaturedArtworkId(Group $group, mixed $featuredArtworkId): ?int { $id = (int) $featuredArtworkId; if ($id <= 0) { return null; } $exists = Artwork::query() ->where('id', $id) ->where('group_id', $group->id) ->whereNull('deleted_at') ->where('artwork_status', 'published') ->where('is_public', true) ->where('is_approved', true) ->whereNotNull('published_at') ->exists(); return $exists ? $id : null; } }