*/ public function pageProps(): array { $now = Carbon::now(); $features = ArtworkFeature::query() ->with([ 'artwork' => fn ($query) => $query->withTrashed()->with([ 'user:id,username,name', 'user.profile:user_id,avatar_hash', 'group:id,name,slug,avatar_path', 'awardStat:artwork_id,gold_count,silver_count,bronze_count,score_total,score_7d,score_30d,last_medaled_at,updated_at', ]), ]) ->orderByDesc('priority') ->orderByDesc('featured_at') ->orderByDesc('id') ->get(); $duplicateCounts = $features->countBy(fn (ArtworkFeature $feature): int => (int) $feature->artwork_id); $entries = $features ->map(fn (ArtworkFeature $feature): array => $this->mapFeature($feature, $duplicateCounts, $now)) ->sort(function (array $left, array $right): int { $comparisons = [ (int) $right['eligibility']['is_eligible'] <=> (int) $left['eligibility']['is_eligible'], (int) $right['is_force_hero'] <=> (int) $left['is_force_hero'], (int) $right['is_active'] <=> (int) $left['is_active'], (int) $left['is_expired'] <=> (int) $right['is_expired'], (int) $right['priority'] <=> (int) $left['priority'], (int) $right['medals']['score_30d'] <=> (int) $left['medals']['score_30d'], $this->timestamp($right['featured_at']) <=> $this->timestamp($left['featured_at']), $this->timestamp($right['artwork']['published_at']) <=> $this->timestamp($left['artwork']['published_at']), (int) $right['id'] <=> (int) $left['id'], ]; foreach ($comparisons as $comparison) { if ($comparison !== 0) { return $comparison; } } return 0; }) ->values(); $eligibleEntries = $this->sortForHeroSelection( $entries->filter(fn (array $entry): bool => (bool) $entry['eligibility']['is_eligible'])->values() ); $sharedWinnerArtworkId = $this->artworks->getFeaturedArtworkWinner()?->id; $winner = $sharedWinnerArtworkId ? $entries->first(fn (array $entry): bool => (int) $entry['artwork_id'] === (int) $sharedWinnerArtworkId) : null; if (! is_array($winner) && $eligibleEntries->isNotEmpty()) { $winner = $eligibleEntries->first(); } $winnerReason = is_array($winner) ? $this->buildWinnerReason($winner, $eligibleEntries) : null; $winnerId = is_array($winner) ? (int) $winner['id'] : null; $entries = $entries ->map(function (array $entry) use ($winnerId, $winnerReason): array { $isWinner = $winnerId !== null && (int) $entry['id'] === $winnerId; if ($isWinner) { array_unshift($entry['status_badges'], [ 'label' => 'Winner', 'tone' => 'amber', ]); } $entry['is_winner'] = $isWinner; $entry['winner_reason'] = $isWinner ? $winnerReason : null; return $entry; }) ->values(); $winner = is_array($winner) ? array_merge($winner, ['selection_reason' => $winnerReason]) : null; return [ 'entries' => $entries->all(), 'winner' => $winner, 'stats' => [ 'total' => $entries->count(), 'active' => $entries->where('is_active', true)->count(), 'inactive' => $entries->where('is_active', false)->count(), 'expired' => $entries->where('is_expired', true)->count(), 'eligible' => $entries->filter(fn (array $entry): bool => (bool) $entry['eligibility']['is_eligible'])->count(), 'ineligible' => $entries->filter(fn (array $entry): bool => ! $entry['eligibility']['is_eligible'])->count(), ], ]; } /** * @return array> */ public function searchArtworks(string $term, int $limit = 12): array { $term = trim($term); if ($term === '') { return []; } $now = Carbon::now(); $artworks = Artwork::query() ->with([ 'user:id,username,name', 'user.profile:user_id,avatar_hash', 'group:id,name,slug,avatar_path', 'awardStat:artwork_id,gold_count,silver_count,bronze_count,score_total,score_7d,score_30d,last_medaled_at,updated_at', ]) ->where(function ($query) use ($term): void { if (ctype_digit($term)) { $query->where('artworks.id', (int) $term); } $query->orWhere('artworks.title', 'like', '%' . $term . '%') ->orWhere('artworks.slug', 'like', '%' . $term . '%') ->orWhereHas('user', function ($userQuery) use ($term): void { $userQuery->where('username', 'like', '%' . $term . '%') ->orWhere('name', 'like', '%' . $term . '%'); }) ->orWhereHas('group', function ($groupQuery) use ($term): void { $groupQuery->where('name', 'like', '%' . $term . '%') ->orWhere('slug', 'like', '%' . $term . '%'); }); }); if (ctype_digit($term)) { $artworks->orderByRaw('CASE WHEN artworks.id = ? THEN 0 ELSE 1 END', [(int) $term]); } $artworks = $artworks ->orderByDesc('published_at') ->limit($limit) ->get(); $featureCounts = ArtworkFeature::query() ->whereNull('deleted_at') ->whereIn('artwork_id', $artworks->pluck('id')) ->selectRaw('artwork_id, COUNT(*) as aggregate') ->groupBy('artwork_id') ->pluck('aggregate', 'artwork_id'); return $artworks ->map(fn (Artwork $artwork): array => $this->mapArtworkCandidate($artwork, (int) ($featureCounts[(int) $artwork->id] ?? 0), $now)) ->values() ->all(); } /** * @param SupportCollection $duplicateCounts * @return array */ private function mapFeature(ArtworkFeature $feature, SupportCollection $duplicateCounts, Carbon $now): array { $context = $this->mapArtworkContext($feature->artwork, $now); $isExpired = $feature->expires_at !== null && $feature->expires_at->lte($now); $isEligible = (bool) $feature->is_active && ! $isExpired && (bool) $context['eligibility']['is_eligible']; $eligibilityReasons = $context['eligibility']['reasons']; if (! $feature->is_active) { $eligibilityReasons[] = 'Inactive'; } if ($isExpired) { $eligibilityReasons[] = 'Expired'; } $statusBadges = []; if ($feature->is_active && ! $isExpired) { $statusBadges[] = ['label' => 'Active', 'tone' => 'emerald']; } if ((bool) $feature->force_hero) { $statusBadges[] = ['label' => 'Force Hero', 'tone' => 'amber']; } if (! $feature->is_active) { $statusBadges[] = ['label' => 'Inactive', 'tone' => 'slate']; } if ($isExpired) { $statusBadges[] = ['label' => 'Expired', 'tone' => 'amber']; } $statusBadges[] = $isEligible ? ['label' => 'Eligible', 'tone' => 'sky'] : ['label' => 'Not eligible', 'tone' => 'rose']; if ((bool) $context['flags']['is_private']) { $statusBadges[] = ['label' => 'Private', 'tone' => 'slate']; } if ((bool) $context['flags']['is_unpublished']) { $statusBadges[] = ['label' => 'Unpublished', 'tone' => 'slate']; } if ((bool) $context['flags']['missing_preview']) { $statusBadges[] = ['label' => 'Missing preview', 'tone' => 'rose']; } if ((bool) $context['flags']['is_deleted']) { $statusBadges[] = ['label' => 'Deleted', 'tone' => 'slate']; } if ((int) $duplicateCounts->get((int) $feature->artwork_id, 0) > 1) { $statusBadges[] = ['label' => 'Duplicate', 'tone' => 'sky']; } return [ 'id' => (int) $feature->id, 'artwork_id' => (int) $feature->artwork_id, 'priority' => (int) $feature->priority, 'featured_at' => $feature->featured_at?->toIsoString(), 'expires_at' => $feature->expires_at?->toIsoString(), 'created_at' => $feature->created_at?->toIsoString(), 'updated_at' => $feature->updated_at?->toIsoString(), 'is_active' => (bool) $feature->is_active, 'is_force_hero' => (bool) $feature->force_hero, 'is_expired' => $isExpired, 'duplicate_count' => (int) $duplicateCounts->get((int) $feature->artwork_id, 0), 'artwork' => $context['artwork'], 'medals' => $context['medals'], 'eligibility' => [ 'is_eligible' => $isEligible, 'reasons' => array_values(array_unique($eligibilityReasons)), ], 'status_badges' => $statusBadges, 'is_winner' => false, 'winner_reason' => null, ]; } /** * @return array */ private function mapArtworkCandidate(Artwork $artwork, int $existingFeatureCount, Carbon $now): array { $context = $this->mapArtworkContext($artwork, $now); return array_merge($context['artwork'], [ 'medals' => $context['medals'], 'eligibility' => $context['eligibility'], 'existing_feature_count' => $existingFeatureCount, 'already_featured' => $existingFeatureCount > 0, ]); } /** * @return array */ private function mapArtworkContext(?Artwork $artwork, Carbon $now): array { if (! $artwork instanceof Artwork) { return [ 'artwork' => [ 'id' => null, 'title' => 'Missing artwork', 'slug' => null, 'canonical_url' => null, 'thumbnail' => ThumbnailPresenter::present(['id' => null, 'name' => 'Missing artwork'], 'sm'), 'published_at' => null, 'visibility' => null, 'is_public' => false, 'is_approved' => false, 'has_missing_preview' => true, 'is_deleted' => true, 'owner' => null, ], 'medals' => [ 'score_30d' => 0, ], 'eligibility' => [ 'is_eligible' => false, 'reasons' => ['Deleted'], ], 'flags' => [ 'is_private' => false, 'is_unpublished' => true, 'missing_preview' => true, 'is_deleted' => true, ], ]; } $isDeleted = $artwork->deleted_at !== null; $isPublic = ! $isDeleted && (bool) $artwork->is_public; $isApproved = ! $isDeleted && (bool) $artwork->is_approved; $isPublished = ! $isDeleted && $artwork->published_at !== null && $artwork->published_at->lte($now); $hasPreview = ! (bool) $artwork->has_missing_thumbnails; $owner = $this->mapOwner($artwork); $reasons = []; if ($isDeleted) { $reasons[] = 'Deleted'; } if (! $isPublic) { $reasons[] = 'Private'; } if (! $isApproved) { $reasons[] = 'Not approved'; } if (! $isPublished) { $reasons[] = 'Unpublished'; } if (! $hasPreview) { $reasons[] = 'Missing preview'; } return [ 'artwork' => [ 'id' => (int) $artwork->id, 'title' => html_entity_decode((string) $artwork->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'slug' => (string) $artwork->slug, 'canonical_url' => route('art.show', ['id' => (int) $artwork->id, 'slug' => $this->artworkSlug($artwork)]), 'thumbnail' => ThumbnailPresenter::present($artwork, 'sm'), 'published_at' => $artwork->published_at?->toIsoString(), 'visibility' => (string) ($artwork->visibility ?? ''), 'is_public' => (bool) $artwork->is_public, 'is_approved' => (bool) $artwork->is_approved, 'has_missing_preview' => (bool) $artwork->has_missing_thumbnails, 'is_deleted' => $isDeleted, 'owner' => $owner, ], 'medals' => [ 'score_30d' => (int) ($artwork->awardStat?->score_30d ?? 0), ], 'eligibility' => [ 'is_eligible' => $isPublic && $isApproved && $isPublished && $hasPreview, 'reasons' => $reasons, ], 'flags' => [ 'is_private' => ! $isPublic, 'is_unpublished' => ! $isPublished, 'missing_preview' => ! $hasPreview, 'is_deleted' => $isDeleted, ], ]; } /** * @param SupportCollection> $entries * @return SupportCollection> */ private function sortForHeroSelection(SupportCollection $entries): SupportCollection { return $entries ->sort(function (array $left, array $right): int { $comparisons = [ (int) $right['is_force_hero'] <=> (int) $left['is_force_hero'], (int) $right['priority'] <=> (int) $left['priority'], (int) $right['medals']['score_30d'] <=> (int) $left['medals']['score_30d'], $this->timestamp($right['featured_at']) <=> $this->timestamp($left['featured_at']), $this->timestamp($right['artwork']['published_at']) <=> $this->timestamp($left['artwork']['published_at']), (int) $right['id'] <=> (int) $left['id'], ]; foreach ($comparisons as $comparison) { if ($comparison !== 0) { return $comparison; } } return 0; }) ->values(); } /** * @param array $winner * @param SupportCollection> $eligibleEntries */ private function buildWinnerReason(array $winner, SupportCollection $eligibleEntries): string { if ((bool) ($winner['is_force_hero'] ?? false)) { return 'Forced hero override is enabled for this featured artwork.'; } $runnerUp = $eligibleEntries->skip(1)->first(); if (! is_array($runnerUp)) { return 'Only eligible featured artwork right now.'; } if ((int) $winner['priority'] > (int) $runnerUp['priority']) { return 'Highest priority among active, eligible featured artworks.'; } if ((int) $winner['medals']['score_30d'] > (int) $runnerUp['medals']['score_30d']) { return 'Tied on priority, won on higher 30-day medal score.'; } if ($this->timestamp($winner['featured_at']) > $this->timestamp($runnerUp['featured_at'])) { return 'Tied on priority and medal score, won on newer featured date.'; } if ($this->timestamp($winner['artwork']['published_at']) > $this->timestamp($runnerUp['artwork']['published_at'])) { return 'Tied on priority, medal score, and featured date, won on newer published date.'; } return 'Selected by the shared homepage hero ordering.'; } /** * @return array|null */ private function mapOwner(Artwork $artwork): ?array { if ($artwork->group) { return [ 'type' => 'group', 'display_name' => (string) $artwork->group->name, 'username' => (string) $artwork->group->slug, 'profile_url' => $artwork->group->publicUrl(), ]; } if (! $artwork->user) { return null; } return [ 'type' => 'user', 'display_name' => (string) ($artwork->user->name ?: '@' . $artwork->user->username), 'username' => (string) $artwork->user->username, 'profile_url' => $artwork->user->username !== '' ? '/@' . $artwork->user->username : null, ]; } private function artworkSlug(Artwork $artwork): string { $slug = trim((string) $artwork->slug); if ($slug !== '') { return $slug; } $titleSlug = Str::slug((string) $artwork->title); return $titleSlug !== '' ? $titleSlug : (string) $artwork->id; } private function timestamp(?string $value): int { if (! is_string($value) || trim($value) === '') { return 0; } return (int) (strtotime($value) ?: 0); } }