$this->getHeroArtwork(), 'trending' => $this->getTrending(), 'fresh' => $this->getFreshUploads(), 'tags' => $this->getPopularTags(), 'creators' => $this->getCreatorSpotlight(), 'news' => $this->getNews(), ]; } // ───────────────────────────────────────────────────────────────────────── // Sections // ───────────────────────────────────────────────────────────────────────── /** * Hero artwork: first item from the featured list. */ public function getHeroArtwork(): ?array { return Cache::remember('homepage.hero', self::CACHE_TTL, function (): ?array { $result = $this->artworks->getFeaturedArtworks(null, 1); /** @var \Illuminate\Database\Eloquent\Model|\null $artwork */ if ($result instanceof \Illuminate\Pagination\LengthAwarePaginator) { $artwork = $result->getCollection()->first(); } elseif ($result instanceof \Illuminate\Support\Collection) { $artwork = $result->first(); } elseif (is_array($result)) { $artwork = $result[0] ?? null; } else { $artwork = null; } return $artwork ? $this->serializeArtwork($artwork, 'lg') : null; }); } /** * Trending: up to 12 artworks ordered by award score, views, downloads, recent activity. * * Award score = SUM(weight × medal_value) where gold=3, silver=2, bronze=1. * Uses correlated subqueries to avoid GROUP BY issues with MySQL strict mode. */ public function getTrending(int $limit = 12): array { return Cache::remember("homepage.trending.{$limit}", self::CACHE_TTL, function () use ($limit): array { $ids = DB::table('artworks') ->select('id') ->selectRaw( '(SELECT COALESCE(SUM(weight * CASE medal' . ' WHEN \'gold\' THEN 3' . ' WHEN \'silver\' THEN 2' . ' ELSE 1 END), 0)' . ' FROM artwork_awards WHERE artwork_awards.artwork_id = artworks.id) AS award_score' ) ->selectRaw('COALESCE((SELECT views FROM artwork_stats WHERE artwork_stats.artwork_id = artworks.id), 0) AS stat_views') ->selectRaw('COALESCE((SELECT downloads FROM artwork_stats WHERE artwork_stats.artwork_id = artworks.id), 0) AS stat_downloads') ->where('is_public', true) ->where('is_approved', true) ->whereNull('deleted_at') ->whereNotNull('published_at') ->where('published_at', '>=', now()->subDays(30)) ->orderByDesc('award_score') ->orderByDesc('stat_views') ->orderByDesc('stat_downloads') ->orderByDesc('published_at') ->limit($limit) ->pluck('id'); if ($ids->isEmpty()) { return []; } $indexed = Artwork::with(['user:id,name,username', 'user.profile:user_id,avatar_hash']) ->whereIn('id', $ids) ->get() ->keyBy('id'); return $ids ->filter(fn ($id) => $indexed->has($id)) ->map(fn ($id) => $this->serializeArtwork($indexed[$id])) ->values() ->all(); }); } /** * Fresh uploads: latest 12 approved public artworks. */ public function getFreshUploads(int $limit = 12): array { return Cache::remember("homepage.fresh.{$limit}", self::CACHE_TTL, function () use ($limit): array { $artworks = Artwork::public() ->published() ->with(['user:id,name,username', 'user.profile:user_id,avatar_hash']) ->orderByDesc('published_at') ->limit($limit) ->get(); return $artworks->map(fn ($a) => $this->serializeArtwork($a))->values()->all(); }); } /** * Top 12 popular tags by usage_count. */ public function getPopularTags(int $limit = 12): array { return Cache::remember("homepage.tags.{$limit}", self::CACHE_TTL, function () use ($limit): array { return Tag::query() ->where('is_active', true) ->orderByDesc('usage_count') ->limit($limit) ->get(['id', 'name', 'slug', 'usage_count']) ->map(fn ($t) => [ 'id' => $t->id, 'name' => $t->name, 'slug' => $t->slug, 'count' => (int) $t->usage_count, ]) ->values() ->all(); }); } /** * Creator spotlight: top 6 creators by weekly uploads, awards, and engagement. * "Weekly uploads" drives ranking per spec; ties broken by total awards then views. */ public function getCreatorSpotlight(int $limit = 6): array { return Cache::remember("homepage.creators.{$limit}", self::CACHE_TTL, function () use ($limit): array { try { $since = now()->subWeek(); $rows = DB::table('artworks') ->join('users as u', 'u.id', '=', 'artworks.user_id') ->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id') ->leftJoin('artwork_awards as aw', 'aw.artwork_id', '=', 'artworks.id') ->leftJoin('artwork_stats as s', 's.artwork_id', '=', 'artworks.id') ->select( 'u.id', 'u.name', 'u.username', 'up.avatar_hash', DB::raw('COUNT(DISTINCT artworks.id) as upload_count'), DB::raw('SUM(CASE WHEN artworks.published_at >= \'' . $since->toDateTimeString() . '\' THEN 1 ELSE 0 END) as weekly_uploads'), DB::raw('COALESCE(SUM(s.views), 0) as total_views'), DB::raw('COUNT(DISTINCT aw.id) as total_awards') ) ->where('artworks.is_public', true) ->where('artworks.is_approved', true) ->whereNull('artworks.deleted_at') ->whereNotNull('artworks.published_at') ->groupBy('u.id', 'u.name', 'u.username', 'up.avatar_hash') ->orderByDesc('weekly_uploads') ->orderByDesc('total_awards') ->orderByDesc('total_views') ->limit($limit) ->get(); $userIds = $rows->pluck('id')->all(); // Pick one random artwork thumbnail per creator for the card background. $thumbsByUser = Artwork::public() ->published() ->whereIn('user_id', $userIds) ->whereNotNull('hash') ->whereNotNull('thumb_ext') ->inRandomOrder() ->get(['id', 'user_id', 'hash', 'thumb_ext']) ->groupBy('user_id'); return $rows->map(function ($u) use ($thumbsByUser) { $artworkForBg = $thumbsByUser->get($u->id)?->first(); $bgThumb = $artworkForBg ? $artworkForBg->thumbUrl('md') : null; return [ 'id' => $u->id, 'name' => $u->name, 'uploads' => (int) $u->upload_count, 'weekly_uploads' => (int) $u->weekly_uploads, 'views' => (int) $u->total_views, 'awards' => (int) $u->total_awards, 'url' => $u->username ? '/@' . $u->username : '/profile/' . $u->id, 'avatar' => AvatarUrl::forUser((int) $u->id, $u->avatar_hash ?: null, 128), 'bg_thumb' => $bgThumb, ]; })->values()->all(); } catch (QueryException $e) { Log::warning('HomepageService::getCreatorSpotlight DB error', [ 'exception' => $e->getMessage(), ]); return []; } }); } /** * Latest 5 news posts from the forum news category. */ public function getNews(int $limit = 5): array { return Cache::remember("homepage.news.{$limit}", self::CACHE_TTL, function () use ($limit): array { try { $items = DB::table('forum_threads as t') ->leftJoin('forum_categories as c', 'c.id', '=', 't.category_id') ->select('t.id', 't.title', 't.created_at', 't.slug as thread_slug') ->where(function ($q) { $q->where('t.category_id', 2876) ->orWhereIn('c.slug', ['news', 'forum-news']); }) ->whereNull('t.deleted_at') ->orderByDesc('t.created_at') ->limit($limit) ->get(); return $items->map(fn ($row) => [ 'id' => $row->id, 'title' => $row->title, 'date' => $row->created_at, 'url' => '/forum/thread/' . $row->id . '-' . ($row->thread_slug ?? 'post'), ])->values()->all(); } catch (QueryException $e) { Log::warning('HomepageService::getNews DB error', [ 'exception' => $e->getMessage(), ]); return []; } }); } // ───────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────── private function serializeArtwork(Artwork $artwork, string $preferSize = 'md'): array { $thumbMd = $artwork->thumbUrl('md'); $thumbLg = $artwork->thumbUrl('lg'); $thumb = $preferSize === 'lg' ? ($thumbLg ?? $thumbMd) : ($thumbMd ?? $thumbLg); $authorId = $artwork->user_id; $authorName = $artwork->user?->name ?? 'Artist'; $authorUsername = $artwork->user?->username ?? ''; $avatarHash = $artwork->user?->profile?->avatar_hash ?? null; $authorAvatar = AvatarUrl::forUser((int) $authorId, $avatarHash, 40); return [ 'id' => $artwork->id, 'title' => $artwork->title ?? 'Untitled', 'slug' => $artwork->slug, 'author' => $authorName, 'author_id' => $authorId, 'author_username' => $authorUsername, 'author_avatar' => $authorAvatar, 'thumb' => $thumb, 'thumb_md' => $thumbMd, 'thumb_lg' => $thumbLg, 'url' => '/art/' . $artwork->id . '/' . ($artwork->slug ?? ''), 'width' => $artwork->width, 'height' => $artwork->height, 'published_at' => $artwork->published_at?->toIso8601String(), ]; } }