From 7a8bc8e22ac79af9001fcba2f6d58670df562228 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 6 May 2026 18:55:20 +0200 Subject: [PATCH] Wire homepage hero to featured thumbnail family; add featured-picture component --- app/Http/Controllers/Web/HomeController.php | 12 +- app/Services/HomepageService.php | 329 +++++++++++------- .../artwork/featured-picture.blade.php | 34 ++ resources/views/web/home.blade.php | 10 +- resources/views/web/home/hero.blade.php | 28 +- 5 files changed, 271 insertions(+), 142 deletions(-) create mode 100644 resources/views/components/artwork/featured-picture.blade.php diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php index 10b3af86..ea1ce91a 100644 --- a/app/Http/Controllers/Web/HomeController.php +++ b/app/Http/Controllers/Web/HomeController.php @@ -16,7 +16,7 @@ final class HomeController extends Controller public function index(Request $request): Response { - $user = $request->user(); + $user = $request->user(); $sections = $user ? $this->homepage->allForUser($user) : array_merge($this->homepage->all(), ['is_logged_in' => false]); @@ -24,17 +24,17 @@ final class HomeController extends Controller $hero = $sections['hero']; $meta = [ - 'title' => 'Skinbase – Digital Art & Wallpapers', + 'title' => 'Skinbase – Digital Art & Wallpapers', 'description' => 'Discover stunning digital art, wallpapers, and skins from a global community of creators. Browse trending works, fresh uploads, and beloved classics.', - 'keywords' => 'wallpapers, digital art, skins, photography, community, wallpaper downloads', - 'og_image' => $hero['thumb_lg'] ?? $hero['thumb'] ?? null, - 'canonical' => url('/'), + 'keywords' => 'wallpapers, digital art, skins, photography, community, wallpaper downloads', + 'og_image' => $hero['featured_image']['preload_url'] ?? $hero['thumb_lg'] ?? $hero['thumb'] ?? null, + 'canonical' => url('/'), ]; $response = response()->view('web.home', [ 'seo' => app(SeoFactory::class)->homepage($meta)->toArray(), 'useUnifiedSeo' => true, - 'meta' => $meta, + 'meta' => $meta, 'props' => $sections, ]); diff --git a/app/Services/HomepageService.php b/app/Services/HomepageService.php index bcc46577..38a203d1 100644 --- a/app/Services/HomepageService.php +++ b/app/Services/HomepageService.php @@ -5,26 +5,25 @@ declare(strict_types=1); namespace App\Services; use App\Models\Artwork; +use App\Models\Collection as CollectionModel; use App\Models\Leaderboard; use App\Models\Tag; -use App\Services\HomepageAnnouncementService; -use App\Services\ArtworkSearchService; +use App\Models\User; use App\Services\EarlyGrowth\EarlyGrowth; use App\Services\EarlyGrowth\GridFiller; +use App\Services\Maturity\ArtworkMaturityService; use App\Services\Recommendations\RecommendationFeedResolver; -use App\Services\UserPreferenceService; use App\Services\Worlds\WorldService; +use App\Support\ArtworkFeaturedImagePath; use App\Support\AvatarUrl; -use App\Models\Collection as CollectionModel; +use cPad\Plugins\News\Models\NewsArticle; use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Illuminate\Database\QueryException; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -use Illuminate\Database\QueryException; -use cPad\Plugins\News\Models\NewsArticle; -use App\Services\Maturity\ArtworkMaturityService; +use Illuminate\Support\Str; /** * HomepageService @@ -36,7 +35,9 @@ use App\Services\Maturity\ArtworkMaturityService; final class HomepageService { private const CACHE_TTL = 300; // 5 minutes + private const DEFAULT_ARTWORK_RAIL_LIMIT = 10; + private const ARTWORK_SERIALIZATION_RELATIONS = [ 'user:id,name,username', 'user.profile:user_id,avatar_hash', @@ -46,12 +47,13 @@ final class HomepageService ]; public function __construct( - private readonly ArtworkService $artworks, - private readonly ArtworkSearchService $search, - private readonly ArtworkMaturityService $maturity, - private readonly UserPreferenceService $prefs, + private readonly ArtworkService $artworks, + private readonly ArtworkSearchService $search, + private readonly ArtworkMaturityService $maturity, + private readonly ArtworkFeaturedImagePath $featuredImages, + private readonly UserPreferenceService $prefs, private readonly RecommendationFeedResolver $feedResolver, - private readonly GridFiller $gridFiller, + private readonly GridFiller $gridFiller, private readonly CollectionDiscoveryService $collectionDiscovery, private readonly CollectionService $collectionService, private readonly CollectionSurfaceService $collectionSurfaces, @@ -118,7 +120,7 @@ final class HomepageService { $configuredStore = (string) config('homepage.cache_store', 'homepage'); - if (is_array(config('cache.stores.' . $configuredStore))) { + if (is_array(config('cache.stores.'.$configuredStore))) { return $configuredStore; } @@ -128,22 +130,22 @@ final class HomepageService private function buildGuestPayload(): array { return [ - 'hero' => $this->getHeroArtwork(), + 'hero' => $this->getHeroArtwork(), 'announcement' => $this->homepageAnnouncements->toHomepagePayload($this->homepageAnnouncements->getActiveForHomepage()), 'community_favorites' => $this->getCommunityFavorites(), 'hall_of_fame' => $this->getHallOfFame(), - 'rising' => $this->getRising(), + 'rising' => $this->getRising(), 'trending' => $this->getTrending(), - 'fresh' => $this->getFreshUploads(), + 'fresh' => $this->getFreshUploads(), 'collections_featured' => $this->getFeaturedCollections(), 'collections_trending' => $this->getTrendingCollections(), 'collections_editorial' => $this->getEditorialCollections(), 'collections_community' => $this->getCommunityCollections(), 'world_spotlight' => $this->worlds->homepageSpotlight(), - 'groups' => $this->getHomepageGroups(), - 'tags' => $this->getPopularTags(), + 'groups' => $this->getHomepageGroups(), + 'tags' => $this->getPopularTags(), 'creators' => $this->getCreatorSpotlight(), - 'news' => $this->getNews(), + 'news' => $this->getNews(), ]; } @@ -167,44 +169,44 @@ final class HomepageService * * Sections: * 1. user_data – welcome row counts (messages, notifications, new followers) - * 2. from_following – artworks from creators you follow - * 3. for_you – personalized recommendation preview - * 4. trending – same trending feed as guests - * 5. by_categories – fresh uploads in user's favourite categories - * 6. suggested_creators – creators the user might want to follow - * 7. tags / creators / news – shared with guest homepage + * 2. from_following – artworks from creators you follow + * 3. for_you – personalized recommendation preview + * 4. trending – same trending feed as guests + * 5. by_categories – fresh uploads in user's favourite categories + * 6. suggested_creators – creators the user might want to follow + * 7. tags / creators / news – shared with guest homepage */ - public function allForUser(\App\Models\User $user): array + public function allForUser(User $user): array { $prefs = $this->prefs->build($user); return [ - 'is_logged_in' => true, - 'user_data' => $this->getUserData($user), - 'hero' => $this->getHeroArtwork(), - 'announcement' => $this->homepageAnnouncements->toHomepagePayload($this->homepageAnnouncements->getActiveForHomepage()), + 'is_logged_in' => true, + 'user_data' => $this->getUserData($user), + 'hero' => $this->getHeroArtwork(), + 'announcement' => $this->homepageAnnouncements->toHomepagePayload($this->homepageAnnouncements->getActiveForHomepage()), 'community_favorites' => $this->getCommunityFavorites(), - 'hall_of_fame' => $this->getHallOfFame(), - 'for_you' => $this->getForYouPreview($user), - 'from_following' => $this->getFollowingFeed($user, $prefs), - 'rising' => $this->getRising(), - 'trending' => $this->getTrending(), - 'fresh' => $this->getFreshUploads(), + 'hall_of_fame' => $this->getHallOfFame(), + 'for_you' => $this->getForYouPreview($user), + 'from_following' => $this->getFollowingFeed($user, $prefs), + 'rising' => $this->getRising(), + 'trending' => $this->getTrending(), + 'fresh' => $this->getFreshUploads(), 'collections_featured' => $this->getFeaturedCollections(), - 'collections_recent' => $this->getRecentCollections(), + 'collections_recent' => $this->getRecentCollections(), 'collections_trending' => $this->getTrendingCollections(), 'collections_editorial' => $this->getEditorialCollections(), 'collections_community' => $this->getCommunityCollections(), 'world_spotlight' => $this->worlds->homepageSpotlight($user), - 'groups' => $this->getHomepageGroups($user), - 'by_tags' => $this->getByTags($prefs['top_tags'] ?? []), - 'by_categories' => $this->getByCategories($prefs['top_categories'] ?? []), - 'suggested_creators' => $this->getSuggestedCreators($user, $prefs), - 'tags' => $this->getPopularTags(), - 'creators' => $this->getCreatorSpotlight(), - 'news' => $this->getNews(), - 'preferences' => [ - 'top_tags' => $prefs['top_tags'] ?? [], + 'groups' => $this->getHomepageGroups($user), + 'by_tags' => $this->getByTags($prefs['top_tags'] ?? []), + 'by_categories' => $this->getByCategories($prefs['top_categories'] ?? []), + 'suggested_creators' => $this->getSuggestedCreators($user, $prefs), + 'tags' => $this->getPopularTags(), + 'creators' => $this->getCreatorSpotlight(), + 'news' => $this->getNews(), + 'preferences' => [ + 'top_tags' => $prefs['top_tags'] ?? [], 'top_categories' => $prefs['top_categories'] ?? [], ], ]; @@ -213,7 +215,7 @@ final class HomepageService /** * "For You" homepage preview backed by the personalized feed engine. */ - public function getForYouPreview(\App\Models\User $user, int $limit = 12): array + public function getForYouPreview(User $user, int $limit = 12): array { try { $feed = $this->feedResolver->getFeed((int) $user->id, max($limit * 3, $limit)); @@ -246,7 +248,7 @@ final class HomepageService 'category_slug' => (string) ($item['category_slug'] ?? ''), 'content_type_name' => (string) ($item['content_type_name'] ?? ''), 'content_type_slug' => (string) ($item['content_type_slug'] ?? ''), - 'url' => (string) ($item['url'] ?? ('/art/' . ((int) ($item['id'] ?? 0)) . '/' . ($item['slug'] ?? ''))), + 'url' => (string) ($item['url'] ?? ('/art/'.((int) ($item['id'] ?? 0)).'/'.($item['slug'] ?? ''))), 'width' => isset($item['width']) ? (int) $item['width'] : null, 'height' => isset($item['height']) ? (int) $item['height'] : null, 'published_at' => $item['published_at'] ?? null, @@ -268,6 +270,7 @@ final class HomepageService })->values()->all(); } catch (\Throwable $e) { Log::warning('HomepageService::getForYouPreview failed', ['error' => $e->getMessage()]); + return []; } } @@ -336,7 +339,7 @@ final class HomepageService ); } - public function getHomepageGroups(?\App\Models\User $viewer = null): array + public function getHomepageGroups(?User $viewer = null): array { if (! $viewer) { return Cache::remember('homepage.groups', self::CACHE_TTL, fn (): array => $this->buildHomepageGroups()); @@ -345,7 +348,7 @@ final class HomepageService return $this->buildHomepageGroups($viewer); } - private function buildHomepageGroups(?\App\Models\User $viewer = null): array + private function buildHomepageGroups(?User $viewer = null): array { $featured = $this->groupDiscovery->surfaceCards($viewer, 'featured', 4); $spotlight = $featured[0] ?? null; @@ -369,7 +372,7 @@ final class HomepageService */ public function getHeroArtwork(): ?array { - return Cache::remember('homepage.hero.' . $this->viewerCacheSegment(), self::CACHE_TTL, function (): ?array { + return Cache::remember('homepage.hero.'.$this->viewerCacheSegment(), self::CACHE_TTL, function (): ?array { $artwork = $this->artworks->getFeaturedArtworkWinner(); if (! $artwork instanceof Artwork) { @@ -387,7 +390,14 @@ final class HomepageService $artwork->loadMissing(self::ARTWORK_SERIALIZATION_RELATIONS); } - return $artwork ? $this->serializeArtwork($artwork, 'lg') : null; + if (! $artwork instanceof Artwork) { + return null; + } + + $payload = $this->serializeArtwork($artwork, 'lg'); + $payload['featured_image'] = $this->serializeFeaturedHeroImage($artwork); + + return $payload; }); } @@ -468,8 +478,8 @@ final class HomepageService return Cache::remember("homepage.rising.{$limit}.{$this->viewerCacheSegment()}", 120, function () use ($limit, $cutoff): array { try { $results = $this->search->searchWithThumbnailPreference([ - 'filter' => 'is_public = true AND is_approved = true AND created_at >= "' . $cutoff . '"', - 'sort' => ['heat_score:desc', 'engagement_velocity:desc', 'created_at:desc'], + 'filter' => 'is_public = true AND is_approved = true AND created_at >= "'.$cutoff.'"', + 'sort' => ['heat_score:desc', 'engagement_velocity:desc', 'created_at:desc'], ], $limit, true, 1); $items = $this->prepareArtworksForSerialization($this->searchResultCollection($results)); @@ -558,8 +568,8 @@ final class HomepageService return Cache::remember("homepage.trending.{$limit}.{$this->viewerCacheSegment()}", self::CACHE_TTL, function () use ($limit, $cutoff): array { try { $results = $this->search->searchWithThumbnailPreference([ - 'filter' => 'is_public = true AND is_approved = true AND created_at >= "' . $cutoff . '"', - 'sort' => ['ranking_score:desc', 'engagement_velocity:desc', 'views:desc'], + 'filter' => 'is_public = true AND is_approved = true AND created_at >= "'.$cutoff.'"', + 'sort' => ['ranking_score:desc', 'engagement_velocity:desc', 'views:desc'], ], $limit, true, 1); $items = $this->prepareArtworksForSerialization($this->searchResultCollection($results)); @@ -614,7 +624,7 @@ final class HomepageService public function getFreshUploads(int $limit = 10): array { // Include EGS mode in cache key so toggling EGS updates the section within TTL - $egsKey = EarlyGrowth::gridFillerEnabled() ? 'egs-' . EarlyGrowth::mode() : 'std'; + $egsKey = EarlyGrowth::gridFillerEnabled() ? 'egs-'.EarlyGrowth::mode() : 'std'; $cacheKey = "homepage.fresh.{$limit}.{$egsKey}.{$this->viewerCacheSegment()}"; return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($limit): array { @@ -646,9 +656,9 @@ final class HomepageService ->limit($limit) ->get(['id', 'name', 'slug', 'usage_count']) ->map(fn ($t) => [ - 'id' => $t->id, - 'name' => $t->name, - 'slug' => $t->slug, + 'id' => $t->id, + 'name' => $t->name, + 'slug' => $t->slug, 'count' => (int) $t->usage_count, ]) ->values() @@ -724,18 +734,18 @@ final class HomepageService return $rows->map(function ($u) use ($thumbsByUser) { $artworkForBg = $thumbsByUser->get($u->id); - $bgThumb = $artworkForBg ? $artworkForBg->thumbUrl('md') : null; + $bgThumb = $artworkForBg ? $artworkForBg->thumbUrl('md') : null; return [ - 'id' => $u->id, - 'name' => $u->name, - 'uploads' => (int) $u->upload_count, + '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, + '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) { @@ -778,7 +788,7 @@ final class HomepageService ->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']); + ->orWhereIn('c.slug', ['news', 'forum-news']); }) ->whereNull('t.deleted_at') ->orderByDesc('t.created_at') @@ -789,7 +799,7 @@ final class HomepageService 'id' => $row->id, 'title' => $row->title, 'date' => $row->created_at, - 'url' => '/forum/thread/' . $row->id . '-' . ($row->thread_slug ?? 'post'), + 'url' => '/forum/thread/'.$row->id.'-'.($row->thread_slug ?? 'post'), ])->values()->all(); } catch (QueryException $e) { Log::warning('HomepageService::getNews DB error', [ @@ -809,7 +819,7 @@ final class HomepageService * Welcome-row counts: unread messages, unread notifications, new followers. * Returns quickly from DB using simple COUNTs; never throws. */ - public function getUserData(\App\Models\User $user): array + public function getUserData(User $user): array { try { $unreadMessages = DB::table('conversations as c') @@ -834,13 +844,13 @@ final class HomepageService } return [ - 'id' => $user->id, - 'name' => $user->name, - 'username' => $user->username, - 'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null, 64), - 'messages_unread' => (int) $unreadMessages, + 'id' => $user->id, + 'name' => $user->name, + 'username' => $user->username, + 'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null, 64), + 'messages_unread' => (int) $unreadMessages, 'notifications_unread' => (int) $unreadNotifications, - 'followers_count' => (int) ($user->statistics?->followers_count ?? 0), + 'followers_count' => (int) ($user->statistics?->followers_count ?? 0), ]; } @@ -848,7 +858,7 @@ final class HomepageService * Suggested creators: active public uploaders NOT already followed by the user, * ranked by follower count. Optionally filtered to the user's top categories. */ - public function getSuggestedCreators(\App\Models\User $user, array $prefs, int $limit = 8): array + public function getSuggestedCreators(User $user, array $prefs, int $limit = 8): array { return Cache::remember( "homepage.suggested.{$user->id}", @@ -878,16 +888,17 @@ final class HomepageService $rows = $query->get(); return $rows->map(fn ($u) => [ - 'id' => $u->id, - 'name' => $u->name, - 'username' => $u->username, - 'url' => $u->username ? '/@' . $u->username : '/profile/' . $u->id, - 'avatar' => AvatarUrl::forUser((int) $u->id, $u->avatar_hash ?: null, 64), + 'id' => $u->id, + 'name' => $u->name, + 'username' => $u->username, + 'url' => $u->username ? '/@'.$u->username : '/profile/'.$u->id, + 'avatar' => AvatarUrl::forUser((int) $u->id, $u->avatar_hash ?: null, 64), 'followers_count' => (int) $u->followers_count, - 'artworks_count' => (int) $u->artworks_count, + 'artworks_count' => (int) $u->artworks_count, ])->values()->all(); } catch (\Throwable $e) { Log::warning('HomepageService::getSuggestedCreators failed', ['error' => $e->getMessage()]); + return []; } } @@ -897,7 +908,7 @@ final class HomepageService /** * Latest artworks from creators the user follows (max 12). */ - public function getFollowingFeed(\App\Models\User $user, array $prefs): array + public function getFollowingFeed(User $user, array $prefs): array { $followingIds = $prefs['followed_creators'] ?? []; @@ -950,6 +961,7 @@ final class HomepageService ->all(); } catch (\Throwable $e) { Log::warning('HomepageService::getByTags failed', ['error' => $e->getMessage()]); + return []; } } @@ -980,6 +992,7 @@ final class HomepageService ->all(); } catch (\Throwable $e) { Log::warning('HomepageService::getByCategories failed', ['error' => $e->getMessage()]); + return []; } } @@ -1010,7 +1023,7 @@ final class HomepageService /** * Ensure serialized artwork payloads do not trigger lazy-loading per item. * - * @param Collection $artworks + * @param Collection $artworks * @return Collection */ private function prepareArtworksForSerialization(Collection $artworks): Collection @@ -1029,7 +1042,7 @@ final class HomepageService /** * Backfill sparse homepage rails with recent archive artworks while preserving lead ordering. * - * @param Collection $artworks + * @param Collection $artworks * @return Collection */ private function fillArtworkRailFromArchive(Collection $artworks, int $limit, ?callable $fallbackConstraint = null): Collection @@ -1069,7 +1082,7 @@ final class HomepageService } /** - * @param Collection> $items + * @param Collection> $items * @return Collection> */ private function filterMissingThumbnailPayloadItems(Collection $items): Collection @@ -1142,50 +1155,50 @@ final class HomepageService $thumbMd = $artwork->thumbUrl('md'); $thumbLg = $artwork->thumbUrl('lg'); $thumbXl = $artwork->thumbUrl('xl'); - $thumb = $preferSize === 'lg' ? ($thumbLg ?? $thumbMd) : ($thumbMd ?? $thumbLg); + $thumb = $preferSize === 'lg' ? ($thumbLg ?? $thumbMd) : ($thumbMd ?? $thumbLg); $primaryCategory = $artwork->categories->sortBy('sort_order')->first(); $thumbSrcset = collect([ - $thumbSm ? $thumbSm . ' 320w' : null, - $thumbMd ? $thumbMd . ' 640w' : null, - $thumbLg ? $thumbLg . ' 1280w' : null, - $thumbXl ? $thumbXl . ' 1920w' : null, + $thumbSm ? $thumbSm.' 320w' : null, + $thumbMd ? $thumbMd.' 640w' : null, + $thumbLg ? $thumbLg.' 1280w' : null, + $thumbXl ? $thumbXl.' 1920w' : null, ])->filter()->implode(', '); $publisher = $this->mapArtworkPublisherPayload($artwork); $isGroupPublisher = ($publisher['type'] ?? null) === 'group'; - $authorId = $artwork->user_id; - $authorName = $isGroupPublisher ? ((string) ($publisher['name'] ?? 'Skinbase Group')) : ($artwork->user?->name ?? 'Artist'); + $authorId = $artwork->user_id; + $authorName = $isGroupPublisher ? ((string) ($publisher['name'] ?? 'Skinbase Group')) : ($artwork->user?->name ?? 'Artist'); $authorUsername = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); - $avatarHash = $artwork->user?->profile?->avatar_hash ?? null; - $authorAvatar = $isGroupPublisher + $avatarHash = $artwork->user?->profile?->avatar_hash ?? null; + $authorAvatar = $isGroupPublisher ? ($publisher['avatar_url'] ?? null) : AvatarUrl::forUser((int) $authorId, $avatarHash, 64); return $this->maturity->decoratePayload([ - 'id' => $artwork->id, - 'title' => $artwork->title ?? 'Untitled', - 'slug' => $artwork->slug, - 'author' => $authorName, - 'author_id' => $authorId, + 'id' => $artwork->id, + 'title' => $artwork->title ?? 'Untitled', + 'slug' => $artwork->slug, + 'author' => $authorName, + 'author_id' => $authorId, 'author_username' => $authorUsername, - 'author_avatar' => $authorAvatar, + 'author_avatar' => $authorAvatar, 'published_as_type' => $artwork->publishedAsType(), - 'publisher' => $publisher, - 'thumb' => $thumb, - 'thumb_sm' => $thumbSm, - 'thumb_md' => $thumbMd, - 'thumb_lg' => $thumbLg, - 'thumb_xl' => $thumbXl, - 'thumb_srcset' => $thumbSrcset !== '' ? $thumbSrcset : null, - 'category_name' => $primaryCategory->name ?? '', - 'category_slug' => $primaryCategory->slug ?? '', + 'publisher' => $publisher, + 'thumb' => $thumb, + 'thumb_sm' => $thumbSm, + 'thumb_md' => $thumbMd, + 'thumb_lg' => $thumbLg, + 'thumb_xl' => $thumbXl, + 'thumb_srcset' => $thumbSrcset !== '' ? $thumbSrcset : null, + 'category_name' => $primaryCategory->name ?? '', + 'category_slug' => $primaryCategory->slug ?? '', 'content_type_name' => $primaryCategory?->contentType?->name ?? '', 'content_type_slug' => $primaryCategory?->contentType?->slug ?? '', - 'url' => '/art/' . $artwork->id . '/' . ($artwork->slug ?? ''), - 'width' => $artwork->width, - 'height' => $artwork->height, - 'published_at' => $artwork->published_at?->toIso8601String(), + 'url' => '/art/'.$artwork->id.'/'.($artwork->slug ?? ''), + 'width' => $artwork->width, + 'height' => $artwork->height, + 'published_at' => $artwork->published_at?->toIso8601String(), 'medals' => [ 'gold' => (int) ($awardStat?->gold_count ?? 0), 'silver' => (int) ($awardStat?->silver_count ?? 0), @@ -1197,6 +1210,80 @@ final class HomepageService ], $artwork, request()->user()); } + /** + * @return array + */ + private function serializeFeaturedHeroImage(Artwork $artwork): array + { + $variants = $this->featuredImages->variants(); + $variantUrls = []; + + foreach (array_keys($variants) as $variant) { + $variantUrls[$variant] = $artwork->hasFeaturedThumbnail($variant) + ? $this->featuredImages->url($artwork, $variant) + : null; + } + + $preloadSrcset = collect($variants) + ->map(function (array $config, string $variant) use ($variantUrls): ?string { + $url = $variantUrls[$variant] ?? null; + + return $url ? $url.' '.(int) $config['width'].'w' : null; + }) + ->filter() + ->implode(', '); + + $xsSources = collect(['xs', 'mobile_sm']) + ->map(function (string $variant) use ($variantUrls, $variants): ?string { + $url = $variantUrls[$variant] ?? null; + + return $url ? $url.' '.(int) ($variants[$variant]['width'] ?? 0).'w' : null; + }) + ->filter() + ->implode(', '); + + $mobileSources = collect(['mobile_sm', 'mobile']) + ->map(function (string $variant) use ($variantUrls, $variants): ?string { + $url = $variantUrls[$variant] ?? null; + + return $url ? $url.' '.(int) ($variants[$variant]['width'] ?? 0).'w' : null; + }) + ->filter() + ->implode(', '); + + $desktopSources = collect(['desktop', 'desktop_xl']) + ->map(function (string $variant) use ($variantUrls, $variants): ?string { + $url = $variantUrls[$variant] ?? null; + + return $url ? $url.' '.(int) ($variants[$variant]['width'] ?? 0).'w' : null; + }) + ->filter() + ->implode(', '); + + $pictureSources = array_values(array_filter([ + $xsSources !== '' ? ['media' => '(max-width: 479px)', 'srcset' => $xsSources, 'sizes' => '100vw'] : null, + $mobileSources !== '' ? ['media' => '(max-width: 767px)', 'srcset' => $mobileSources, 'sizes' => '100vw'] : null, + ! empty($variantUrls['tablet']) ? ['media' => '(max-width: 1279px)', 'srcset' => $variantUrls['tablet'].' '.(int) ($variants['tablet']['width'] ?? 0).'w', 'sizes' => '100vw'] : null, + $desktopSources !== '' ? ['media' => '(min-width: 1280px)', 'srcset' => $desktopSources, 'sizes' => '100vw'] : null, + ])); + + return [ + 'alt' => $artwork->featuredImageAltText(), + 'variants' => $variantUrls, + 'sources' => $pictureSources, + 'img_src' => $artwork->featuredThumbnailUrl('desktop'), + 'img_srcset' => $preloadSrcset !== '' ? $preloadSrcset : ($artwork->thumb_srcset ?? null), + 'img_sizes' => '100vw', + 'preload_url' => $variantUrls['desktop_xl'] + ?? $variantUrls['desktop'] + ?? $artwork->thumbUrl('xl') + ?? $artwork->thumbUrl('lg') + ?? 'https://files.skinbase.org/default/missing_xl.webp', + 'preload_srcset' => $preloadSrcset !== '' ? $preloadSrcset : ($artwork->thumb_srcset ?? null), + 'preload_sizes' => '100vw', + ]; + } + /** * @return array|null */ @@ -1233,8 +1320,8 @@ final class HomepageService $payload['metric_badge'] = [ 'label' => $surface === 'community_favorites' - ? '30d medals: ' . $score - : 'All-time medals: ' . $score, + ? '30d medals: '.$score + : 'All-time medals: '.$score, 'className' => $surface === 'community_favorites' ? 'bg-amber-500/14 text-amber-100 ring-amber-300/30' : 'bg-cyan-500/14 text-cyan-100 ring-cyan-300/30', @@ -1245,6 +1332,6 @@ final class HomepageService private function viewerCacheSegment(): string { - return 'visibility-' . $this->maturity->viewerPreferences(request()->user())['visibility']; + return 'visibility-'.$this->maturity->viewerPreferences(request()->user())['visibility']; } } diff --git a/resources/views/components/artwork/featured-picture.blade.php b/resources/views/components/artwork/featured-picture.blade.php new file mode 100644 index 00000000..12303ecd --- /dev/null +++ b/resources/views/components/artwork/featured-picture.blade.php @@ -0,0 +1,34 @@ +@props([ + 'image' => [], + 'fetchpriority' => 'auto', + 'loading' => 'lazy', + 'decoding' => 'async', +]) + +@php + $sources = is_array($image['sources'] ?? null) ? $image['sources'] : []; + $src = $image['img_src'] ?? 'https://files.skinbase.org/default/missing_xl.webp'; + $srcset = $image['img_srcset'] ?? null; + $sizes = $image['img_sizes'] ?? '100vw'; + $alt = $image['alt'] ?? 'Featured artwork'; +@endphp + + + @foreach ($sources as $source) + + @endforeach + {{ $alt }} + \ No newline at end of file diff --git a/resources/views/web/home.blade.php b/resources/views/web/home.blade.php index cec1182d..e6db47e8 100644 --- a/resources/views/web/home.blade.php +++ b/resources/views/web/home.blade.php @@ -4,7 +4,15 @@ @push('head') {{-- Preload hero image for faster LCP --}} - @if(!empty($props['hero']['thumb']) || !empty($props['hero']['thumb_lg'])) + @if(!empty($props['hero']['featured_image']['preload_url'])) + + @elseif(!empty($props['hero']['thumb']) || !empty($props['hero']['thumb_lg'])) @else
- - {{ $heroArtwork['title'] ?? 'Featured artwork' }} - +