Wire homepage hero to featured thumbnail family; add featured-picture component
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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<int, Artwork> $artworks
|
||||
* @param Collection<int, Artwork> $artworks
|
||||
* @return Collection<int, Artwork>
|
||||
*/
|
||||
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<int, Artwork> $artworks
|
||||
* @param Collection<int, Artwork> $artworks
|
||||
* @return Collection<int, Artwork>
|
||||
*/
|
||||
private function fillArtworkRailFromArchive(Collection $artworks, int $limit, ?callable $fallbackConstraint = null): Collection
|
||||
@@ -1069,7 +1082,7 @@ final class HomepageService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, array<string, mixed>> $items
|
||||
* @param Collection<int, array<string, mixed>> $items
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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<string, mixed>|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'];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user