feat: add reusable gallery carousel and ranking feed infrastructure

This commit is contained in:
2026-02-28 07:56:25 +01:00
parent 67ef79766c
commit 6536d4ae78
36 changed files with 3177 additions and 373 deletions

View File

@@ -38,7 +38,7 @@ final class DiscoverController extends Controller
{
$perPage = 24;
$results = $this->searchService->discoverTrending($perPage);
$artworks = $results->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
$this->hydrateDiscoverSearchResults($results);
return view('web.discover.index', [
'artworks' => $results,
@@ -55,7 +55,7 @@ final class DiscoverController extends Controller
{
$perPage = 24;
$results = $this->searchService->discoverFresh($perPage);
$results->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
$this->hydrateDiscoverSearchResults($results);
return view('web.discover.index', [
'artworks' => $results,
@@ -72,7 +72,7 @@ final class DiscoverController extends Controller
{
$perPage = 24;
$results = $this->searchService->discoverTopRated($perPage);
$results->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
$this->hydrateDiscoverSearchResults($results);
return view('web.discover.index', [
'artworks' => $results,
@@ -89,7 +89,7 @@ final class DiscoverController extends Controller
{
$perPage = 24;
$results = $this->searchService->discoverMostDownloaded($perPage);
$results->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
$this->hydrateDiscoverSearchResults($results);
return view('web.discover.index', [
'artworks' => $results,
@@ -110,7 +110,11 @@ final class DiscoverController extends Controller
$artworks = Artwork::query()
->public()
->published()
->with(['user:id,name', 'categories:id,name,slug,content_type_id,parent_id,sort_order'])
->with([
'user:id,name',
'user.profile:user_id,avatar_hash',
'categories:id,name,slug,content_type_id,parent_id,sort_order',
])
->whereRaw('MONTH(published_at) = ?', [$today->month])
->whereRaw('DAY(published_at) = ?', [$today->day])
->whereRaw('YEAR(published_at) < ?', [$today->year])
@@ -206,16 +210,27 @@ final class DiscoverController extends Controller
$artworkItems = $feedResult['data'] ?? [];
// Build a simple presentable collection
$artworks = collect($artworkItems)->map(fn (array $item) => (object) [
'id' => $item['id'] ?? 0,
'name' => $item['title'] ?? 'Untitled',
'category_name' => '',
'thumb_url' => $item['thumbnail_url'] ?? null,
'thumb_srcset' => $item['thumbnail_url'] ?? null,
'uname' => $item['author'] ?? 'Artist',
'published_at' => null,
'slug' => $item['slug'] ?? '',
]);
$artworks = collect($artworkItems)->map(function (array $item) {
$width = isset($item['width']) && $item['width'] > 0 ? (int) $item['width'] : null;
$height = isset($item['height']) && $item['height'] > 0 ? (int) $item['height'] : null;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($item['author_id'] ?? 0), null, 64);
return (object) [
'id' => $item['id'] ?? 0,
'name' => $item['title'] ?? 'Untitled',
'category_name' => $item['category_name'] ?? '',
'category_slug' => $item['category_slug'] ?? '',
'thumb_url' => $item['thumbnail_url'] ?? null,
'thumb_srcset' => $item['thumbnail_url'] ?? null,
'uname' => $item['author'] ?? 'Artist',
'username' => $item['username'] ?? '',
'avatar_url' => $avatarUrl,
'published_at' => $item['published_at'] ?? null,
'slug' => $item['slug'] ?? '',
'width' => $width,
'height' => $height,
];
});
$meta = $feedResult['meta'] ?? [];
$nextCursor = $meta['next_cursor'] ?? null;
@@ -308,10 +323,73 @@ final class DiscoverController extends Controller
// ─── Helpers ─────────────────────────────────────────────────────────────
private function hydrateDiscoverSearchResults($paginator): void
{
if (!is_object($paginator) || !method_exists($paginator, 'getCollection') || !method_exists($paginator, 'setCollection')) {
return;
}
$items = $paginator->getCollection();
if (!$items || $items->isEmpty()) {
return;
}
$ids = $items
->pluck('id')
->filter(fn ($id) => is_numeric($id) && (int) $id > 0)
->map(fn ($id) => (int) $id)
->values();
if ($ids->isEmpty()) {
return;
}
$byId = Artwork::query()
->whereIn('id', $ids)
->with([
'user:id,name,username',
'user.profile:user_id,avatar_hash',
'categories:id,name,slug,content_type_id,parent_id,sort_order',
])
->get()
->keyBy('id');
$paginator->setCollection(
$items->map(function ($item) use ($byId) {
$id = (int) ($item->id ?? 0);
$full = $id > 0 ? $byId->get($id) : null;
if ($full instanceof Artwork) {
return $this->presentArtwork($full);
}
return (object) [
'id' => $item->id ?? 0,
'name' => $item->title ?? $item->name ?? 'Untitled',
'category_name' => $item->category_name ?? $item->category ?? '',
'category_slug' => $item->category_slug ?? '',
'thumb_url' => $item->thumbnail_url ?? $item->thumb_url ?? $item->thumb ?? null,
'thumb_srcset' => $item->thumb_srcset ?? null,
'uname' => $item->author ?? $item->uname ?? 'Skinbase',
'username' => $item->username ?? '',
'avatar_url' => \App\Support\AvatarUrl::forUser((int) ($item->user_id ?? $item->author_id ?? 0), null, 64),
'published_at' => $item->published_at ?? null,
'width' => isset($item->width) && $item->width ? (int) $item->width : null,
'height' => isset($item->height) && $item->height ? (int) $item->height : null,
];
})
);
}
private function presentArtwork(Artwork $artwork): object
{
$primaryCategory = $artwork->categories->sortBy('sort_order')->first();
$present = ThumbnailPresenter::present($artwork, 'md');
$avatarUrl = \App\Support\AvatarUrl::forUser(
(int) ($artwork->user_id ?? 0),
$artwork->user?->profile?->avatar_hash ?? null,
64
);
return (object) [
'id' => $artwork->id,
@@ -322,6 +400,7 @@ final class DiscoverController extends Controller
'thumb_url' => $present['url'],
'thumb_srcset' => $present['srcset'] ?? $present['url'],
'uname' => $artwork->user->name ?? 'Skinbase',
'avatar_url' => $avatarUrl,
'published_at' => $artwork->published_at,
'width' => $artwork->width ?? null,
'height' => $artwork->height ?? null,