with(['user', 'stats']) ->public() ->published() ->orderByDesc('trending_score_7d') ->limit($limit) ->get() ->map(fn (Artwork $a) => $this->artworkCard($a)); }); } // ── Similar tags by slug prefix / Levenshtein approximation (max 10) ───── public function similarTags(string $slug, int $limit = 10): Collection { $limit = min($limit, 10); $prefix = substr($slug, 0, 3); return Cache::remember("error_suggestions.similar_tags.{$slug}.{$limit}", self::CACHE_TTL, function () use ($slug, $limit, $prefix) { return Tag::query() ->where('slug', '!=', $slug) ->where(function ($q) use ($prefix, $slug) { $q->where('slug', 'like', $prefix . '%') ->orWhere('slug', 'like', '%' . substr($slug, -3) . '%'); }) ->orderByDesc('artworks_count') ->limit($limit) ->get(['id', 'name', 'slug', 'artworks_count']); }); } // ── Trending tags (max 10) ──────────────────────────────────────────────── public function trendingTags(int $limit = 10): Collection { $limit = min($limit, 10); return Cache::remember("error_suggestions.tags.{$limit}", self::CACHE_TTL, function () use ($limit) { return Tag::query() ->orderByDesc('artworks_count') ->limit($limit) ->get(['id', 'name', 'slug', 'artworks_count']); }); } // ── Trending creators (max 6) ───────────────────────────────────────────── public function trendingCreators(int $limit = 6): Collection { $limit = min($limit, 6); return Cache::remember("error_suggestions.creators.{$limit}", self::CACHE_TTL, function () use ($limit) { return DB::table('users as u') ->join('user_statistics as us', 'us.user_id', '=', 'u.id') ->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id') ->select('u.id', 'u.name', 'u.username', 'up.avatar_hash', DB::raw('us.uploads_count as artworks_count')) ->where('u.is_active', true) ->whereNull('u.deleted_at') ->where('us.uploads_count', '>', 0) ->orderByDesc('us.uploads_count') ->limit($limit) ->get() ->map(fn ($u) => $this->creatorCardFromRow($u)); }); } // ── Recently joined creators (max 6) ───────────────────────────────────── public function recentlyJoinedCreators(int $limit = 6): Collection { $limit = min($limit, 6); return Cache::remember("error_suggestions.creators.recent.{$limit}", self::CACHE_TTL, function () use ($limit) { return DB::table('users as u') ->join('user_statistics as us', 'us.user_id', '=', 'u.id') ->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id') ->select('u.id', 'u.name', 'u.username', 'up.avatar_hash', DB::raw('us.uploads_count as artworks_count')) ->where('u.is_active', true) ->whereNull('u.deleted_at') ->where('us.uploads_count', '>', 0) ->orderByDesc('u.id') ->limit($limit) ->get() ->map(fn ($u) => $this->creatorCardFromRow($u)); }); } // ── Latest blog posts (max 6) ───────────────────────────────────────────── public function latestBlogPosts(int $limit = 6): Collection { $limit = min($limit, 6); return Cache::remember("error_suggestions.blog.{$limit}", self::CACHE_TTL, function () use ($limit) { return BlogPost::published() ->orderByDesc('published_at') ->limit($limit) ->get(['id', 'title', 'slug', 'excerpt', 'published_at']) ->map(fn ($p) => [ 'id' => $p->id, 'title' => $p->title, 'excerpt' => Str::limit($p->excerpt ?? '', 100), 'url' => '/blog/' . $p->slug, 'published_at' => $p->published_at?->diffForHumans(), ]); }); } // ── Private helpers ─────────────────────────────────────────────────────── private function artworkCard(Artwork $a): array { $slug = Str::slug((string) ($a->slug ?: $a->title)) ?: (string) $a->id; $md = ThumbnailPresenter::present($a, 'md'); return [ 'id' => $a->id, 'title' => html_entity_decode((string) $a->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'author' => html_entity_decode((string) ($a->user?->name ?: $a->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'url' => route('art.show', ['id' => $a->id, 'slug' => $slug]), 'thumb' => $md['url'] ?? null, 'thumb_srcset' => $md['srcset'] ?? null, ]; } private function creatorCard(User $u, int $artworksCount = 0): array { return [ 'id' => $u->id, 'name' => $u->name ?: $u->username, 'username' => $u->username, 'url' => '/@' . $u->username, 'avatar_url' => \App\Support\AvatarUrl::forUser( (int) $u->id, optional($u->profile)->avatar_hash, 64 ), 'artworks_count' => $artworksCount, ]; } private function creatorCardFromRow(object $u): array { return [ 'id' => (int) $u->id, 'name' => $u->name ?: $u->username, 'username' => $u->username, 'url' => '/@' . $u->username, 'avatar_url' => \App\Support\AvatarUrl::forUser( (int) $u->id, $u->avatar_hash ?? null, 64 ), 'artworks_count' => (int) ($u->artworks_count ?? 0), ]; } }