publicEligible() ->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at']); $this->applySharedFilters($query, $filters, false); $this->applyPublicSort($query, (string) ($filters['sort'] ?? 'trending')); return $query->paginate(max(1, min($perPage, 24)))->withQueryString(); } public function ownerSearch(User $user, array $filters, int $perPage = 20): LengthAwarePaginator { $query = Collection::query() ->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at']) ->when(! $user->isAdmin() && ! $user->isModerator(), fn ($builder) => $builder->where('user_id', $user->id)); $this->applySharedFilters($query, $filters, true); return $query ->orderByDesc('updated_at') ->paginate(max(1, min($perPage, 50))) ->withQueryString(); } public function publicFilterOptions(): array { $themeOptions = Collection::query() ->publicEligible() ->whereNotNull('theme_token') ->where('theme_token', '!=', '') ->select('theme_token') ->distinct() ->orderBy('theme_token') ->limit(12) ->pluck('theme_token') ->map(fn ($token): array => [ 'value' => (string) $token, 'label' => $this->humanizeToken((string) $token), ]) ->values() ->all(); $categoryOptions = Category::query() ->active() ->orderBy('sort_order') ->orderBy('name') ->limit(16) ->get(['slug', 'name']) ->map(fn (Category $category): array => [ 'value' => (string) $category->slug, 'label' => (string) $category->name, ]) ->values() ->all(); $styleOptions = collect((array) config('collections.smart_rules.style_terms', [])) ->map(fn ($term): array => [ 'value' => (string) $term, 'label' => $this->humanizeToken((string) $term), ]) ->values() ->all(); $colorOptions = collect((array) config('collections.smart_rules.color_terms', [])) ->map(fn ($term): array => [ 'value' => (string) $term, 'label' => $this->humanizeToken((string) $term), ]) ->values() ->all(); return [ 'category' => $categoryOptions, 'style' => $styleOptions, 'theme' => $themeOptions, 'color' => $colorOptions, 'quality_tier' => [ ['value' => 'editorial', 'label' => 'Editorial'], ['value' => 'high', 'label' => 'High'], ['value' => 'standard', 'label' => 'Standard'], ['value' => 'limited', 'label' => 'Limited'], ], ]; } private function applySharedFilters($query, array $filters, bool $includeInternal): void { if (filled($filters['q'] ?? null)) { $term = '%' . trim((string) $filters['q']) . '%'; $query->where(function ($builder) use ($term): void { $builder->where('title', 'like', $term) ->orWhere('summary', 'like', $term) ->orWhere('description', 'like', $term) ->orWhere('campaign_label', 'like', $term) ->orWhere('series_title', 'like', $term) ->orWhereHas('user', function (Builder $userQuery) use ($term): void { $userQuery->where('username', 'like', $term) ->orWhere('name', 'like', $term); }); }); } foreach (['type', 'visibility', 'lifecycle_state', 'mode', 'campaign_key', 'program_key', 'workflow_state', 'health_state'] as $field) { if (filled($filters[$field] ?? null)) { $query->where($field, (string) $filters[$field]); } } if (filled($filters['quality_tier'] ?? null)) { $query->where('trust_tier', (string) $filters['quality_tier']); } if (filled($filters['theme'] ?? null)) { $theme = trim((string) $filters['theme']); $themeLike = '%' . mb_strtolower($theme) . '%'; $query->where(function (Builder $builder) use ($theme, $themeLike): void { $builder->whereRaw('LOWER(theme_token) = ?', [mb_strtolower($theme)]) ->orWhereHas('entityLinks', function (Builder $linkQuery) use ($themeLike): void { $linkQuery->where('linked_type', CollectionLinkService::TYPE_TAG) ->whereExists(function ($tagQuery) use ($themeLike): void { $tagQuery->select(DB::raw('1')) ->from('tags') ->whereColumn('tags.id', 'collection_entity_links.linked_id') ->where(function ($tagBuilder) use ($themeLike): void { $tagBuilder->whereRaw('LOWER(tags.slug) like ?', [$themeLike]) ->orWhereRaw('LOWER(tags.name) like ?', [$themeLike]); }); }); }) ->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($themeLike): void { $tagQuery->whereRaw('LOWER(tags.slug) like ?', [$themeLike]) ->orWhereRaw('LOWER(tags.name) like ?', [$themeLike]); }); }); } if (filled($filters['category'] ?? null)) { $category = trim((string) $filters['category']); $categoryLike = '%' . mb_strtolower($category) . '%'; $query->where(function (Builder $builder) use ($categoryLike): void { $builder->whereHas('entityLinks', function (Builder $linkQuery) use ($categoryLike): void { $linkQuery->where('linked_type', CollectionLinkService::TYPE_CATEGORY) ->whereExists(function ($categoryQuery) use ($categoryLike): void { $categoryQuery->select(DB::raw('1')) ->from('categories') ->whereColumn('categories.id', 'collection_entity_links.linked_id') ->where(function ($inner) use ($categoryLike): void { $inner->whereRaw('LOWER(categories.slug) like ?', [$categoryLike]) ->orWhereRaw('LOWER(categories.name) like ?', [$categoryLike]); }); }); })->orWhereHas('artworks.categories', function (Builder $categoryQuery) use ($categoryLike): void { $categoryQuery->whereRaw('LOWER(categories.slug) like ?', [$categoryLike]) ->orWhereRaw('LOWER(categories.name) like ?', [$categoryLike]); }); }); } if (filled($filters['style'] ?? null)) { $style = trim((string) $filters['style']); $styleLike = '%' . mb_strtolower($style) . '%'; $query->where(function (Builder $builder) use ($styleLike): void { $builder->whereRaw('LOWER(spotlight_style) like ?', [$styleLike]) ->orWhereRaw('LOWER(presentation_style) like ?', [$styleLike]) ->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($styleLike): void { $tagQuery->whereRaw('LOWER(tags.slug) like ?', [$styleLike]) ->orWhereRaw('LOWER(tags.name) like ?', [$styleLike]); }); }); } if (filled($filters['color'] ?? null)) { $color = trim((string) $filters['color']); $colorLike = '%' . mb_strtolower($color) . '%'; $query->where(function (Builder $builder) use ($colorLike): void { $builder->whereRaw('LOWER(theme_token) like ?', [$colorLike]) ->orWhereHas('artworks.tags', function (Builder $tagQuery) use ($colorLike): void { $tagQuery->whereRaw('LOWER(tags.slug) like ?', [$colorLike]) ->orWhereRaw('LOWER(tags.name) like ?', [$colorLike]); }); }); } if (filled($filters['placement_eligibility'] ?? null) && $includeInternal) { $query->where('placement_eligibility', filter_var($filters['placement_eligibility'], FILTER_VALIDATE_BOOLEAN)); } if (filled($filters['partner_key'] ?? null) && $includeInternal) { $query->where('partner_key', (string) $filters['partner_key']); } if (filled($filters['experiment_key'] ?? null) && $includeInternal) { $query->where('experiment_key', (string) $filters['experiment_key']); } } private function applyPublicSort($query, string $sort): void { match ($sort) { 'recent' => $query->orderByDesc('published_at')->orderByDesc('updated_at'), 'quality' => $query->orderByDesc('health_score')->orderByDesc('quality_score'), 'evergreen' => $query->orderByDesc('quality_score')->orderByDesc('followers_count'), default => $query->orderByDesc('ranking_score')->orderByDesc('health_score')->orderByDesc('updated_at'), }; } private function humanizeToken(string $value): string { return str($value) ->replace(['_', '-'], ' ') ->title() ->value(); } }