providers()) ->map(fn (CreatorStudioProvider $provider): array => $provider->summary($user)) ->values() ->all(); } public function quickCreate(): array { $preferredOrder = ['artworks', 'cards', 'stories', 'collections']; return SupportCollection::make($this->providers()) ->sortBy(fn (CreatorStudioProvider $provider): int => array_search($provider->key(), $preferredOrder, true)) ->map(fn (CreatorStudioProvider $provider): array => [ 'key' => $provider->key(), 'label' => rtrim($provider->label(), 's'), 'icon' => $provider->icon(), 'url' => $provider->createUrl(), ]) ->values() ->all(); } public function list(User $user, array $filters = [], ?string $fixedBucket = null, ?string $fixedModule = null): array { $module = $fixedModule ?: $this->normalizeModule((string) ($filters['module'] ?? 'all')); $bucket = $fixedBucket ?: $this->normalizeBucket((string) ($filters['bucket'] ?? 'all')); $search = trim((string) ($filters['q'] ?? '')); $sort = $this->normalizeSort((string) ($filters['sort'] ?? 'updated_desc')); $category = (string) ($filters['category'] ?? 'all'); $tag = trim((string) ($filters['tag'] ?? '')); $visibility = (string) ($filters['visibility'] ?? 'all'); $activityState = (string) ($filters['activity_state'] ?? 'all'); $stale = (string) ($filters['stale'] ?? 'all'); $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = min(max((int) ($filters['per_page'] ?? 24), 12), 48); $items = $module === 'all' ? SupportCollection::make($this->providers())->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, $this->providerBucket($bucket), 200)) : $this->provider($module)?->items($user, $this->providerBucket($bucket), 240) ?? SupportCollection::make(); if ($bucket === 'featured') { $items = $items->filter(fn (array $item): bool => (bool) ($item['featured'] ?? false)); } elseif ($bucket === 'recent') { $items = $items->filter(function (array $item): bool { $date = $item['published_at'] ?? $item['updated_at'] ?? $item['created_at'] ?? null; return $date !== null && strtotime((string) $date) >= Carbon::now()->subDays(30)->getTimestamp(); }); } if ($search !== '') { $needle = mb_strtolower($search); $items = $items->filter(function (array $item) use ($needle): bool { $haystacks = [ $item['title'] ?? '', $item['subtitle'] ?? '', $item['description'] ?? '', $item['module_label'] ?? '', ]; return SupportCollection::make($haystacks) ->filter(fn ($value): bool => is_string($value) && $value !== '') ->contains(fn (string $value): bool => str_contains(mb_strtolower($value), $needle)); }); } if ($module === 'artworks' && $category !== 'all') { $items = $items->filter(function (array $item) use ($category): bool { return SupportCollection::make($item['taxonomies']['categories'] ?? []) ->contains(fn (array $entry): bool => (string) ($entry['slug'] ?? '') === $category); }); } if ($module === 'artworks' && $tag !== '') { $needle = mb_strtolower($tag); $items = $items->filter(function (array $item) use ($needle): bool { return SupportCollection::make($item['taxonomies']['tags'] ?? []) ->contains(fn (array $entry): bool => str_contains(mb_strtolower((string) ($entry['name'] ?? '')), $needle)); }); } if ($module === 'collections' && $visibility !== 'all') { $items = $items->filter(fn (array $item): bool => (string) ($item['visibility'] ?? '') === $visibility); } if ($module === 'stories' && $activityState !== 'all') { $items = $items->filter(fn (array $item): bool => (string) ($item['activity_state'] ?? 'all') === $activityState); } if ($stale === 'only') { $threshold = Carbon::now()->subDays(3)->getTimestamp(); $items = $items->filter(function (array $item) use ($threshold): bool { $updatedAt = strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? '')); return $updatedAt > 0 && $updatedAt <= $threshold; }); } $items = $this->annotateItems($this->sortItems($items, $sort)->values()); $total = $items->count(); $lastPage = max(1, (int) ceil($total / $perPage)); $page = min($page, $lastPage); return [ 'items' => $items->forPage($page, $perPage)->values()->all(), 'meta' => [ 'current_page' => $page, 'last_page' => $lastPage, 'per_page' => $perPage, 'total' => $total, ], 'filters' => [ 'module' => $module, 'bucket' => $bucket, 'q' => $search, 'sort' => $sort, 'category' => $category, 'tag' => $tag, 'visibility' => $visibility, 'activity_state' => $activityState, 'stale' => $stale, ], 'module_options' => array_merge([ ['value' => 'all', 'label' => 'All content'], ], SupportCollection::make($this->moduleSummaries($user))->map(fn (array $summary): array => [ 'value' => $summary['key'], 'label' => $summary['label'], ])->all()), 'bucket_options' => [ ['value' => 'all', 'label' => 'All'], ['value' => 'published', 'label' => 'Published'], ['value' => 'drafts', 'label' => 'Drafts'], ['value' => 'scheduled', 'label' => 'Scheduled'], ['value' => 'archived', 'label' => 'Archived'], ['value' => 'featured', 'label' => 'Featured'], ['value' => 'recent', 'label' => 'Recent'], ], 'sort_options' => [ ['value' => 'updated_desc', 'label' => 'Recently updated'], ['value' => 'updated_asc', 'label' => 'Oldest untouched'], ['value' => 'created_desc', 'label' => 'Newest created'], ['value' => 'published_desc', 'label' => 'Newest published'], ['value' => 'views_desc', 'label' => 'Most viewed'], ['value' => 'appreciation_desc', 'label' => 'Most liked'], ['value' => 'comments_desc', 'label' => 'Most commented'], ['value' => 'engagement_desc', 'label' => 'Best engagement'], ['value' => 'title_asc', 'label' => 'Title A-Z'], ], 'advanced_filters' => $this->advancedFilters($module, $items, [ 'category' => $category, 'tag' => $tag, 'visibility' => $visibility, 'activity_state' => $activityState, 'stale' => $stale, ]), ]; } public function draftReminders(User $user, int $limit = 4): array { return $this->annotateItems(SupportCollection::make($this->providers()) ->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'drafts', $limit)) ->sortByDesc(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? Carbon::now()->toIso8601String()))) ->take($limit) ->values()) ->all(); } public function staleDrafts(User $user, int $limit = 4): array { return $this->annotateItems(SupportCollection::make($this->providers()) ->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'drafts', $limit * 4)) ->filter(function (array $item): bool { $updatedAt = strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? Carbon::now()->toIso8601String())); return $updatedAt <= Carbon::now()->subDays(3)->getTimestamp(); }) ->sortBy(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? Carbon::now()->toIso8601String()))) ->take($limit) ->values()) ->all(); } public function continueWorking(User $user, string $draftBehavior = 'resume-last', int $limit = 3): array { if ($draftBehavior === 'focus-published') { return $this->recentPublished($user, $limit); } return $this->annotateItems(SupportCollection::make($this->providers()) ->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'drafts', $limit * 4)) ->sortByDesc(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? Carbon::now()->toIso8601String()))) ->take($limit) ->values()) ->all(); } public function recentPublished(User $user, int $limit = 6): array { return $this->annotateItems(SupportCollection::make($this->providers()) ->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'published', $limit * 2)) ->sortByDesc(fn (array $item): int => strtotime((string) ($item['published_at'] ?? $item['updated_at'] ?? Carbon::now()->toIso8601String()))) ->take($limit) ->values()) ->all(); } public function featuredCandidates(User $user, int $limit = 8): array { return $this->annotateItems(SupportCollection::make($this->providers()) ->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'published', $limit * 2)) ->filter(fn (array $item): bool => ($item['status'] ?? null) === 'published') ->sortByDesc(fn (array $item): int => (int) ($item['engagement_score'] ?? 0)) ->take($limit) ->values()) ->all(); } /** * @param array $featuredContent * @return array|null> */ public function selectedItems(User $user, array $featuredContent): array { return SupportCollection::make(['artworks', 'cards', 'collections', 'stories']) ->mapWithKeys(function (string $module) use ($user, $featuredContent): array { $selectedId = (int) ($featuredContent[$module] ?? 0); if ($selectedId < 1) { return [$module => null]; } $item = $this->provider($module)?->items($user, 'all', 400) ->first(fn (array $entry): bool => (int) ($entry['numeric_id'] ?? 0) === $selectedId); return [$module => $item ?: null]; }) ->all(); } public function provider(string $module): ?CreatorStudioProvider { return SupportCollection::make($this->providers())->first(fn (CreatorStudioProvider $provider): bool => $provider->key() === $module); } public function providers(): array { return [ $this->artworks, $this->cards, $this->collections, $this->stories, ]; } private function normalizeModule(string $module): string { $allowed = ['all', 'artworks', 'cards', 'collections', 'stories']; return in_array($module, $allowed, true) ? $module : 'all'; } private function normalizeBucket(string $bucket): string { $allowed = ['all', 'published', 'drafts', 'scheduled', 'archived', 'featured', 'recent']; return in_array($bucket, $allowed, true) ? $bucket : 'all'; } private function normalizeSort(string $sort): string { $allowed = ['updated_desc', 'updated_asc', 'created_desc', 'published_desc', 'views_desc', 'appreciation_desc', 'comments_desc', 'engagement_desc', 'title_asc']; return in_array($sort, $allowed, true) ? $sort : 'updated_desc'; } private function providerBucket(string $bucket): string { return $bucket === 'featured' ? 'published' : $bucket; } private function sortItems(SupportCollection $items, string $sort): SupportCollection { return match ($sort) { 'created_desc' => $items->sortByDesc(fn (array $item): int => strtotime((string) ($item['created_at'] ?? ''))), 'updated_asc' => $items->sortBy(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? ''))), 'published_desc' => $items->sortByDesc(fn (array $item): int => strtotime((string) ($item['published_at'] ?? $item['updated_at'] ?? ''))), 'views_desc' => $items->sortByDesc(fn (array $item): int => (int) (($item['metrics']['views'] ?? 0))), 'appreciation_desc' => $items->sortByDesc(fn (array $item): int => (int) (($item['metrics']['appreciation'] ?? 0))), 'comments_desc' => $items->sortByDesc(fn (array $item): int => (int) (($item['metrics']['comments'] ?? 0))), 'engagement_desc' => $items->sortByDesc(fn (array $item): int => (int) ($item['engagement_score'] ?? 0)), 'title_asc' => $items->sortBy(fn (array $item): string => mb_strtolower((string) ($item['title'] ?? ''))), default => $items->sortByDesc(fn (array $item): int => strtotime((string) ($item['updated_at'] ?? $item['created_at'] ?? ''))), }; } /** * @param array $currentFilters */ private function advancedFilters(string $module, SupportCollection $items, array $currentFilters): array { return match ($module) { 'artworks' => [ [ 'key' => 'category', 'label' => 'Category', 'type' => 'select', 'value' => $currentFilters['category'] ?? 'all', 'options' => array_merge([ ['value' => 'all', 'label' => 'All categories'], ], $items ->flatMap(fn (array $item) => $item['taxonomies']['categories'] ?? []) ->unique('slug') ->sortBy('name') ->map(fn (array $entry): array => [ 'value' => (string) ($entry['slug'] ?? ''), 'label' => (string) ($entry['name'] ?? 'Category'), ])->values()->all()), ], [ 'key' => 'tag', 'label' => 'Tag', 'type' => 'search', 'value' => $currentFilters['tag'] ?? '', 'placeholder' => 'Filter by tag', ], ], 'collections' => [[ 'key' => 'visibility', 'label' => 'Visibility', 'type' => 'select', 'value' => $currentFilters['visibility'] ?? 'all', 'options' => [ ['value' => 'all', 'label' => 'All visibility'], ['value' => 'public', 'label' => 'Public'], ['value' => 'unlisted', 'label' => 'Unlisted'], ['value' => 'private', 'label' => 'Private'], ], ]], 'stories' => [[ 'key' => 'activity_state', 'label' => 'Activity', 'type' => 'select', 'value' => $currentFilters['activity_state'] ?? 'all', 'options' => [ ['value' => 'all', 'label' => 'All states'], ['value' => 'active', 'label' => 'Active'], ['value' => 'inactive', 'label' => 'Inactive'], ['value' => 'archived', 'label' => 'Archived'], ], ]], 'all' => [[ 'key' => 'stale', 'label' => 'Draft freshness', 'type' => 'select', 'value' => $currentFilters['stale'] ?? 'all', 'options' => [ ['value' => 'all', 'label' => 'All drafts'], ['value' => 'only', 'label' => 'Stale drafts'], ], ]], default => [[ 'key' => 'stale', 'label' => 'Draft freshness', 'type' => 'select', 'value' => $currentFilters['stale'] ?? 'all', 'options' => [ ['value' => 'all', 'label' => 'All drafts'], ['value' => 'only', 'label' => 'Stale drafts'], ], ]], }; } private function annotateItems(SupportCollection $items): SupportCollection { return $items->map(fn (array $item): array => $this->annotateItem($item))->values(); } private function annotateItem(array $item): array { $now = Carbon::now(); $updatedAt = Carbon::parse((string) ($item['updated_at'] ?? $item['created_at'] ?? $now->toIso8601String())); $isDraft = ($item['status'] ?? null) === 'draft'; $missing = []; $score = 0; if ($this->hasValue($item['title'] ?? null)) { $score++; } else { $missing[] = 'Add a title'; } if ($this->hasValue($item['description'] ?? null)) { $score++; } else { $missing[] = 'Add a description'; } if ($this->hasValue($item['image_url'] ?? null)) { $score++; } else { $missing[] = 'Add a preview image'; } if (! empty($item['taxonomies']['categories'] ?? []) || $this->hasValue($item['subtitle'] ?? null)) { $score++; } else { $missing[] = 'Choose a category or content context'; } $label = match (true) { $score >= 4 => 'Ready to publish', $score === 3 => 'Almost ready', default => 'Needs more work', }; $workflowActions = match ((string) ($item['module'] ?? '')) { 'artworks' => [ ['label' => 'Create card', 'href' => route('studio.cards.create'), 'icon' => 'fa-solid fa-id-card'], ['label' => 'Curate collection', 'href' => route('settings.collections.create'), 'icon' => 'fa-solid fa-layer-group'], ['label' => 'Tell story', 'href' => route('creator.stories.create'), 'icon' => 'fa-solid fa-feather-pointed'], ], 'cards' => [ ['label' => 'Build collection', 'href' => route('settings.collections.create'), 'icon' => 'fa-solid fa-layer-group'], ['label' => 'Open stories', 'href' => route('studio.stories'), 'icon' => 'fa-solid fa-feather-pointed'], ], 'stories' => [ ['label' => 'Create card', 'href' => route('studio.cards.create'), 'icon' => 'fa-solid fa-id-card'], ['label' => 'Curate collection', 'href' => route('settings.collections.create'), 'icon' => 'fa-solid fa-layer-group'], ], 'collections' => [ ['label' => 'Review artworks', 'href' => route('studio.artworks'), 'icon' => 'fa-solid fa-images'], ['label' => 'Review cards', 'href' => route('studio.cards.index'), 'icon' => 'fa-solid fa-id-card'], ], default => [], }; $item['workflow'] = [ 'is_stale_draft' => $isDraft && $updatedAt->lte($now->copy()->subDays(3)), 'last_touched_days' => max(0, $updatedAt->diffInDays($now)), 'resume_label' => $isDraft ? 'Resume draft' : 'Open item', 'readiness' => [ 'score' => $score, 'max' => 4, 'label' => $label, 'can_publish' => $score >= 3, 'missing' => $missing, ], 'cross_module_actions' => $workflowActions, ]; return $item; } private function hasValue(mixed $value): bool { return is_string($value) ? trim($value) !== '' : ! empty($value); } }