feat: ship creator journey v2 and profile updates
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Services\ContentSanitizer;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Services\ErrorSuggestionService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use App\Support\AvatarUrl;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -23,7 +24,10 @@ use Illuminate\View\View;
|
||||
|
||||
final class ArtworkPageController extends Controller
|
||||
{
|
||||
public function __construct(private readonly GroupService $groups) {}
|
||||
public function __construct(
|
||||
private readonly GroupService $groups,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, int $id, ?string $slug = null): View|RedirectResponse|Response
|
||||
{
|
||||
@@ -145,6 +149,7 @@ final class ArtworkPageController extends Controller
|
||||
->whereKeyNot($artwork->id)
|
||||
->public()
|
||||
->published()
|
||||
->tap(fn ($builder) => $this->maturity->applyViewerFilter($builder, $request->user()))
|
||||
->where(function ($query) use ($artwork, $categoryIds, $tagIds): void {
|
||||
$query->where('user_id', $artwork->user_id);
|
||||
|
||||
@@ -176,14 +181,14 @@ final class ArtworkPageController extends Controller
|
||||
$md = ThumbnailPresenter::present($item, 'md');
|
||||
$lg = ThumbnailPresenter::present($item, 'lg');
|
||||
|
||||
return [
|
||||
return $this->maturity->decoratePayload([
|
||||
'id' => (int) $item->id,
|
||||
'title' => html_entity_decode((string) $item->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'author' => html_entity_decode((string) ($item->group?->name ?: $item->user?->name ?: $item->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'url' => route('art.show', ['id' => $item->id, 'slug' => $itemSlug]),
|
||||
'thumb' => $md['url'] ?? null,
|
||||
'thumb_srcset' => ($md['url'] ?? '') . ' 640w, ' . ($lg['url'] ?? '') . ' 1280w',
|
||||
];
|
||||
], $item, request()->user());
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
@@ -7,7 +7,10 @@ use App\Models\ContentType;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkSearchService;
|
||||
use App\Services\ArtworkService;
|
||||
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -17,8 +20,6 @@ use Illuminate\Pagination\AbstractCursorPaginator;
|
||||
|
||||
class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
{
|
||||
private const CONTENT_TYPE_SLUGS = ['photography', 'wallpapers', 'skins', 'other', 'digital-art'];
|
||||
|
||||
/**
|
||||
* Meilisearch sort-field arrays per sort alias.
|
||||
* First element is primary sort; subsequent elements are tie-breakers.
|
||||
@@ -74,6 +75,8 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
public function __construct(
|
||||
private ArtworkService $artworks,
|
||||
private ArtworkSearchService $search,
|
||||
private ContentTypeSlugResolver $contentTypeResolver,
|
||||
private ArtworkMaturityService $maturity,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -121,14 +124,18 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
|
||||
public function content(Request $request, string $contentTypeSlug, ?string $path = null)
|
||||
{
|
||||
$contentSlug = strtolower($contentTypeSlug);
|
||||
if (! in_array($contentSlug, self::CONTENT_TYPE_SLUGS, true)) {
|
||||
$requestedSlug = strtolower($contentTypeSlug);
|
||||
$resolution = $this->contentTypeResolver->resolve($requestedSlug);
|
||||
|
||||
if (! $resolution->found() || $resolution->contentType === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$contentType = ContentType::where('slug', $contentSlug)->first();
|
||||
if (! $contentType) {
|
||||
abort(404);
|
||||
$contentType = $resolution->contentType;
|
||||
$contentSlug = strtolower((string) $contentType->slug);
|
||||
|
||||
if ($resolution->requiresRedirect()) {
|
||||
return $this->redirectToContentTypePath($request, $contentSlug, $path, 301);
|
||||
}
|
||||
|
||||
// Default sort: trending (not chronological)
|
||||
@@ -265,12 +272,25 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
$contentTypeSlug = strtolower((string) $contentTypeSlug);
|
||||
$categoryPath = $categoryPath !== null ? trim((string) $categoryPath, '/') : (isset($pathSegments[1]) ? implode('/', array_slice($pathSegments, 1, max(0, count($pathSegments) - 2))) : '');
|
||||
|
||||
$resolution = $this->contentTypeResolver->resolve($contentTypeSlug);
|
||||
if (! $resolution->found() || $resolution->contentType === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$resolvedContentTypeSlug = strtolower((string) $resolution->contentType->slug);
|
||||
|
||||
// Normalize artwork param if route-model binding returned an Artwork model
|
||||
$artworkSlug = $artwork instanceof Artwork ? (string) $artwork->slug : (string) $artwork;
|
||||
|
||||
if ($resolution->requiresRedirect()) {
|
||||
$path = trim($categoryPath . '/' . $artworkSlug, '/');
|
||||
|
||||
return $this->redirectToContentTypePath($req, $resolvedContentTypeSlug, $path, 301);
|
||||
}
|
||||
|
||||
return app(\App\Http\Controllers\ArtworkController::class)->show(
|
||||
$req,
|
||||
$contentTypeSlug,
|
||||
$resolvedContentTypeSlug,
|
||||
$categoryPath,
|
||||
$artworkSlug
|
||||
);
|
||||
@@ -293,7 +313,7 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
$username = $isGroupPublisher ? '' : ($artwork->user?->username ?? '');
|
||||
$profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null);
|
||||
|
||||
return (object) [
|
||||
return (object) $this->maturity->decoratePayload([
|
||||
'id' => $artwork->id,
|
||||
'name' => $artwork->title,
|
||||
'content_type_name' => $primaryCategory?->contentType?->name ?? '',
|
||||
@@ -317,7 +337,7 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
'published_at' => $artwork->published_at,
|
||||
'width' => $artwork->width ?? null,
|
||||
'height' => $artwork->height ?? null,
|
||||
];
|
||||
], $artwork, request()->user());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,9 +392,8 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
|
||||
private function mainCategories(): Collection
|
||||
{
|
||||
return ContentType::ordered()
|
||||
->whereIn('slug', self::CONTENT_TYPE_SLUGS)
|
||||
->get(['name', 'slug'])
|
||||
return $this->contentTypeResolver
|
||||
->publicContentTypes()
|
||||
->map(function (ContentType $type) {
|
||||
return (object) [
|
||||
'id' => $type->id,
|
||||
@@ -385,6 +404,18 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
|
||||
});
|
||||
}
|
||||
|
||||
private function redirectToContentTypePath(Request $request, string $contentTypeSlug, ?string $path = null, int $status = 301): RedirectResponse
|
||||
{
|
||||
$target = url('/' . trim($contentTypeSlug . '/' . trim((string) $path, '/'), '/'));
|
||||
$queryString = $request->getQueryString();
|
||||
|
||||
if ($queryString) {
|
||||
$target .= '?' . $queryString;
|
||||
}
|
||||
|
||||
return redirect()->to($target, $status);
|
||||
}
|
||||
|
||||
private function buildPaginationSeo(Request $request, string $canonicalBaseUrl, mixed $paginator): array
|
||||
{
|
||||
$canonicalQuery = $request->query();
|
||||
|
||||
@@ -5,13 +5,14 @@ namespace App\Http\Controllers\Web;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkService;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DailyUploadsController extends Controller
|
||||
{
|
||||
protected ArtworkService $artworks;
|
||||
|
||||
public function __construct(ArtworkService $artworks)
|
||||
public function __construct(ArtworkService $artworks, private readonly ArtworkMaturityService $maturity)
|
||||
{
|
||||
$this->artworks = $artworks;
|
||||
}
|
||||
@@ -76,11 +77,11 @@ class DailyUploadsController extends Controller
|
||||
|
||||
private function prepareArts($ars)
|
||||
{
|
||||
return $ars->map(function (Artwork $ar) {
|
||||
$items = $ars->map(function (Artwork $ar): array {
|
||||
$primaryCategory = $ar->categories->sortBy('sort_order')->first();
|
||||
$present = \App\Services\ThumbnailPresenter::present($ar, 'md');
|
||||
|
||||
return (object) [
|
||||
return $this->maturity->decoratePayload([
|
||||
'id' => $ar->id,
|
||||
'name' => $ar->title,
|
||||
'thumb' => $present['url'],
|
||||
@@ -88,7 +89,11 @@ class DailyUploadsController extends Controller
|
||||
'gid_num' => $primaryCategory ? ((int) $primaryCategory->id % 5) * 5 : 0,
|
||||
'category_name' => $primaryCategory->name ?? '',
|
||||
'uname' => $ar->user->name ?? 'Skinbase',
|
||||
];
|
||||
});
|
||||
], $ar, request()->user());
|
||||
})->values()->all();
|
||||
|
||||
return collect($this->maturity->filterPayloadItems($items, request()->user()))
|
||||
->map(static fn (array $item): object => (object) $item)
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ use App\Services\ArtworkSearchService;
|
||||
use App\Services\ArtworkService;
|
||||
use App\Services\EarlyGrowth\AdaptiveTimeWindow;
|
||||
use App\Services\EarlyGrowth\GridFiller;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\Recommendations\RecommendationFeedResolver;
|
||||
use App\Services\UserSuggestionService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -38,6 +40,7 @@ final class DiscoverController extends Controller
|
||||
private readonly GridFiller $gridFiller,
|
||||
private readonly CommunityActivityService $communityActivity,
|
||||
private readonly UserSuggestionService $userSuggestions,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
) {}
|
||||
|
||||
// ─── /discover/trending ──────────────────────────────────────────────────
|
||||
@@ -178,6 +181,7 @@ final class DiscoverController extends Controller
|
||||
->whereRaw('MONTH(published_at) = ?', [$today->month])
|
||||
->whereRaw('DAY(published_at) = ?', [$today->day])
|
||||
->whereRaw('YEAR(published_at) < ?', [$today->year])
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('published_at')
|
||||
->paginate($perPage)
|
||||
->withQueryString();
|
||||
@@ -270,7 +274,8 @@ final class DiscoverController extends Controller
|
||||
|
||||
$artworks = collect($feedResult['data'] ?? [])->map(
|
||||
fn (array $item) => $this->presentRecommendedArtwork($item)
|
||||
)->values();
|
||||
);
|
||||
$artworks = $this->reorderDiscoverItemsByThumbnailHealth($artworks)->values();
|
||||
|
||||
$meta = $feedResult['meta'] ?? [];
|
||||
$nextCursor = $meta['next_cursor'] ?? null;
|
||||
@@ -345,6 +350,7 @@ final class DiscoverController extends Controller
|
||||
->published()
|
||||
->with(['user:id,name,username', 'categories:id,name,slug,content_type_id,parent_id,sort_order'])
|
||||
->whereIn('user_id', $followingIds)
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('published_at')
|
||||
->paginate($perPage)
|
||||
->withQueryString();
|
||||
@@ -416,6 +422,7 @@ final class DiscoverController extends Controller
|
||||
'categories:id,name,slug,content_type_id,parent_id,sort_order',
|
||||
'categories.contentType:id,slug,name',
|
||||
])
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('published_at')
|
||||
->orderByDesc('id')
|
||||
->paginate($perPage)
|
||||
@@ -438,6 +445,7 @@ final class DiscoverController extends Controller
|
||||
->leftJoin('artwork_stats as discover_stats', 'discover_stats.artwork_id', '=', 'artworks.id')
|
||||
->select('artworks.*')
|
||||
->where('artworks.published_at', '>=', $cutoff)
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('discover_stats.ranking_score')
|
||||
->orderByDesc('discover_stats.engagement_velocity')
|
||||
->orderByDesc('discover_stats.views')
|
||||
@@ -465,6 +473,7 @@ final class DiscoverController extends Controller
|
||||
->selectRaw('COALESCE(discover_stats.heat_score, 0) as heat_score')
|
||||
->selectRaw('COALESCE(discover_stats.engagement_velocity, 0) as engagement_velocity')
|
||||
->where('artworks.published_at', '>=', $cutoff)
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('discover_stats.heat_score')
|
||||
->orderByDesc('discover_stats.engagement_velocity')
|
||||
->orderByDesc('artworks.published_at')
|
||||
@@ -496,6 +505,7 @@ final class DiscoverController extends Controller
|
||||
->selectRaw('COALESCE(discover_stats.engagement_velocity, 0) as engagement_velocity')
|
||||
->selectRaw('COALESCE(recent_rising_activity.recent_signal_24h, 0) as recent_signal_24h')
|
||||
->where('artworks.published_at', '>=', $cutoff)
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc('recent_signal_24h')
|
||||
->orderByDesc('artworks.published_at')
|
||||
->orderByDesc('artworks.id')
|
||||
@@ -599,7 +609,7 @@ final class DiscoverController extends Controller
|
||||
$username = $isGroupPublisher ? '' : ($artwork->user?->username ?? '');
|
||||
$profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null);
|
||||
|
||||
return (object) [
|
||||
return (object) $this->maturity->decoratePayload([
|
||||
'id' => $artwork->id,
|
||||
'name' => $artwork->title,
|
||||
'content_type_name' => $primaryCategory?->contentType?->name ?? '',
|
||||
@@ -624,7 +634,7 @@ final class DiscoverController extends Controller
|
||||
'published_at' => $artwork->published_at,
|
||||
'width' => $artwork->width ?? null,
|
||||
'height' => $artwork->height ?? null,
|
||||
];
|
||||
], $artwork, request()->user());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -676,6 +686,7 @@ final class DiscoverController extends Controller
|
||||
->whereIn('user_id', $followingIds)
|
||||
->where('published_at', '>=', now()->subDays(30))
|
||||
->leftJoin('artwork_stats as ast', 'ast.artwork_id', '=', 'artworks.id')
|
||||
->orderMissingThumbnailsLast()
|
||||
->orderByDesc(DB::raw('COALESCE(ast.heat_score, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(ast.favorites, 0)'))
|
||||
->orderByDesc('artworks.published_at')
|
||||
@@ -703,4 +714,42 @@ final class DiscoverController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, object> $items
|
||||
* @return Collection<int, object>
|
||||
*/
|
||||
private function reorderDiscoverItemsByThumbnailHealth(Collection $items): Collection
|
||||
{
|
||||
if ($items->isEmpty()) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$ids = $items
|
||||
->pluck('id')
|
||||
->filter(fn ($id) => is_numeric($id) && (int) $id > 0)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->values();
|
||||
|
||||
if ($ids->isEmpty()) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$missingIds = Artwork::query()
|
||||
->whereIn('id', $ids)
|
||||
->where('has_missing_thumbnails', true)
|
||||
->pluck('id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->flip();
|
||||
|
||||
if ($missingIds->isEmpty()) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$healthy = $items->reject(fn ($item) => $missingIds->has((int) ($item->id ?? 0)));
|
||||
|
||||
return $healthy
|
||||
->concat($items->filter(fn ($item) => $missingIds->has((int) ($item->id ?? 0))))
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ContentType;
|
||||
use App\Services\ArtworkSearchService;
|
||||
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
||||
use App\Services\EarlyGrowth\EarlyGrowth;
|
||||
use App\Services\EarlyGrowth\GridFiller;
|
||||
use App\Services\EarlyGrowth\SpotlightEngineInterface;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\AbstractCursorPaginator;
|
||||
@@ -27,8 +28,6 @@ use Illuminate\Support\Facades\Cache;
|
||||
*/
|
||||
final class ExploreController extends Controller
|
||||
{
|
||||
private const CONTENT_TYPE_SLUGS = ['artworks', 'wallpapers', 'skins', 'photography', 'other'];
|
||||
|
||||
/** Meilisearch sort-field arrays per sort alias. */
|
||||
private const SORT_MAP = [
|
||||
'trending' => ['trending_score_24h:desc', 'trending_score_7d:desc', 'favorites_count:desc', 'created_at:desc'],
|
||||
@@ -65,6 +64,8 @@ final class ExploreController extends Controller
|
||||
private readonly ArtworkSearchService $search,
|
||||
private readonly GridFiller $gridFiller,
|
||||
private readonly SpotlightEngineInterface $spotlight,
|
||||
private readonly ContentTypeSlugResolver $contentTypeResolver,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
) {}
|
||||
|
||||
// ── /explore (hub) ──────────────────────────────────────────────────
|
||||
@@ -75,13 +76,15 @@ final class ExploreController extends Controller
|
||||
$perPage = $this->resolvePerPage($request);
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$ttl = self::SORT_TTL[$sort] ?? 300;
|
||||
$cacheVersion = $this->cacheVersion();
|
||||
|
||||
$artworks = Cache::remember("explore.all.{$sort}.{$page}", $ttl, fn () =>
|
||||
Artwork::search('')->options([
|
||||
$artworks = Cache::remember("explore.all.v{$cacheVersion}.{$sort}.{$page}", $ttl, fn () =>
|
||||
$this->search->searchWithThumbnailPreference([
|
||||
'filter' => 'is_public = true AND is_approved = true',
|
||||
'sort' => self::SORT_MAP[$sort] ?? ['created_at:desc'],
|
||||
])->paginate($perPage)
|
||||
], $perPage, false, $page)
|
||||
);
|
||||
$artworks = $this->filterBrowsableArtworks($artworks);
|
||||
// EGS: fill grid to minimum when uploads are sparse
|
||||
$artworks = $this->gridFiller->fill($artworks, 0, $page);
|
||||
$artworks->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
|
||||
@@ -121,35 +124,43 @@ final class ExploreController extends Controller
|
||||
|
||||
public function byType(Request $request, string $type)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
if (!in_array($type, self::CONTENT_TYPE_SLUGS, true)) {
|
||||
$resolution = $this->contentTypeResolver->resolve($type, allowVirtual: true);
|
||||
|
||||
if (! $resolution->found()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// "artworks" is the umbrella — search all types
|
||||
$isAll = $type === 'artworks';
|
||||
$isAll = $resolution->isVirtual && $resolution->virtualType === 'artworks';
|
||||
|
||||
if (! $isAll && $resolution->contentType === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$resolvedTypeSlug = $isAll ? 'artworks' : strtolower((string) $resolution->contentType->slug);
|
||||
|
||||
// Canonical URLs for content types are /skins, /wallpapers, /photography, /other.
|
||||
if (! $isAll) {
|
||||
return redirect()->to($this->canonicalTypeUrl($request, $type), 301);
|
||||
return redirect()->to($this->canonicalTypeUrl($request, $resolvedTypeSlug), 301);
|
||||
}
|
||||
|
||||
$sort = $this->resolveSort($request);
|
||||
$perPage = $this->resolvePerPage($request);
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$ttl = self::SORT_TTL[$sort] ?? 300;
|
||||
$cacheVersion = $this->cacheVersion();
|
||||
|
||||
$filter = 'is_public = true AND is_approved = true';
|
||||
if (!$isAll) {
|
||||
$filter .= ' AND content_type = "' . $type . '"';
|
||||
}
|
||||
|
||||
$artworks = Cache::remember("explore.{$type}.{$sort}.{$page}", $ttl, fn () =>
|
||||
Artwork::search('')->options([
|
||||
$artworks = Cache::remember("explore.{$resolvedTypeSlug}.v{$cacheVersion}.{$sort}.{$page}", $ttl, fn () =>
|
||||
$this->search->searchWithThumbnailPreference([
|
||||
'filter' => $filter,
|
||||
'sort' => self::SORT_MAP[$sort] ?? ['created_at:desc'],
|
||||
])->paginate($perPage)
|
||||
], $perPage, false, $page)
|
||||
);
|
||||
$artworks = $this->filterBrowsableArtworks($artworks);
|
||||
// EGS: fill grid to minimum when uploads are sparse
|
||||
$artworks = $this->gridFiller->fill($artworks, 0, $page);
|
||||
$artworks->getCollection()->transform(fn ($a) => $this->presentArtwork($a));
|
||||
@@ -163,7 +174,7 @@ final class ExploreController extends Controller
|
||||
$contentType = null;
|
||||
$subcategories = $mainCategories;
|
||||
if (! $isAll) {
|
||||
$contentType = ContentType::where('slug', $type)->first();
|
||||
$contentType = $resolution->contentType;
|
||||
$subcategories = $contentType
|
||||
? $contentType->rootCategories()->orderBy('sort_order')->orderBy('name')->get()
|
||||
: collect();
|
||||
@@ -172,10 +183,10 @@ final class ExploreController extends Controller
|
||||
if ($isAll) {
|
||||
$humanType = 'Artworks';
|
||||
} else {
|
||||
$humanType = $contentType?->name ?? ucfirst($type);
|
||||
$humanType = $contentType?->name ?? ucfirst($resolvedTypeSlug);
|
||||
}
|
||||
|
||||
$baseUrl = url('/explore/' . $type);
|
||||
$baseUrl = url('/explore/' . $resolvedTypeSlug);
|
||||
$seo = $this->paginationSeo($request, $baseUrl, $artworks);
|
||||
|
||||
return view('gallery.index', [
|
||||
@@ -192,11 +203,11 @@ final class ExploreController extends Controller
|
||||
'hero_description' => "Browse {$humanType} on Skinbase.",
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Explore', 'url' => '/explore'],
|
||||
(object) ['name' => $humanType, 'url' => "/explore/{$type}"],
|
||||
(object) ['name' => $humanType, 'url' => "/explore/{$resolvedTypeSlug}"],
|
||||
]),
|
||||
'page_title' => "{$humanType} - Explore - Skinbase",
|
||||
'page_meta_description' => "Discover the best {$humanType} artworks on Skinbase. Browse trending, new and top-rated.",
|
||||
'page_meta_keywords' => strtolower($type) . ', explore, skinbase, artworks, wallpapers, skins, photography',
|
||||
'page_meta_keywords' => strtolower($resolvedTypeSlug) . ', explore, skinbase, artworks, wallpapers, skins, photography',
|
||||
'page_canonical' => $seo['canonical'],
|
||||
'page_rel_prev' => $seo['prev'],
|
||||
'page_rel_next' => $seo['next'],
|
||||
@@ -208,12 +219,17 @@ final class ExploreController extends Controller
|
||||
|
||||
public function byTypeMode(Request $request, string $type, string $mode)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
if ($type !== 'artworks') {
|
||||
$resolution = $this->contentTypeResolver->resolve($type, allowVirtual: true);
|
||||
|
||||
if (! $resolution->found()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (! ($resolution->isVirtual && $resolution->virtualType === 'artworks')) {
|
||||
$query = $request->query();
|
||||
$query['sort'] = $this->normalizeSort((string) $mode);
|
||||
|
||||
return redirect()->to($this->canonicalTypeUrl($request, $type, $query), 301);
|
||||
return redirect()->to($this->canonicalTypeUrl($request, strtolower((string) $resolution->contentType?->slug), $query), 301);
|
||||
}
|
||||
|
||||
// Rewrite the sort via the URL segment and delegate
|
||||
@@ -225,8 +241,8 @@ final class ExploreController extends Controller
|
||||
|
||||
private function mainCategories(): Collection
|
||||
{
|
||||
$categories = ContentType::ordered()
|
||||
->get(['name', 'slug'])
|
||||
$categories = $this->contentTypeResolver
|
||||
->publicContentTypes()
|
||||
->map(fn ($ct) => (object) [
|
||||
'name' => $ct->name,
|
||||
'slug' => $ct->slug,
|
||||
@@ -272,6 +288,26 @@ final class ExploreController extends Controller
|
||||
return max(12, min($v, 80));
|
||||
}
|
||||
|
||||
private function cacheVersion(): int
|
||||
{
|
||||
return max(1, (int) Cache::get('explore.cache.version', 1));
|
||||
}
|
||||
|
||||
private function filterBrowsableArtworks(AbstractPaginator $paginator): AbstractPaginator
|
||||
{
|
||||
$paginator->setCollection(
|
||||
$paginator->getCollection()
|
||||
->filter(fn ($artwork) => $artwork instanceof Artwork
|
||||
&& $artwork->deleted_at === null
|
||||
&& (bool) $artwork->is_public
|
||||
&& (bool) $artwork->is_approved
|
||||
&& $artwork->published_at !== null)
|
||||
->values()
|
||||
);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
private function presentArtwork(Artwork $artwork): object
|
||||
{
|
||||
$primary = $artwork->categories->sortBy('sort_order')->first();
|
||||
@@ -289,7 +325,7 @@ final class ExploreController extends Controller
|
||||
$username = $isGroupPublisher ? '' : ($artwork->user?->username ?? '');
|
||||
$profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null);
|
||||
|
||||
return (object) [
|
||||
return (object) $this->maturity->decoratePayload([
|
||||
'id' => $artwork->id,
|
||||
'name' => $artwork->title,
|
||||
'content_type_name' => $primary?->contentType?->name ?? '',
|
||||
@@ -314,7 +350,7 @@ final class ExploreController extends Controller
|
||||
'slug' => $artwork->slug ?? '',
|
||||
'width' => $artwork->width ?? null,
|
||||
'height' => $artwork->height ?? null,
|
||||
];
|
||||
], $artwork, request()->user());
|
||||
}
|
||||
|
||||
private function paginationSeo(Request $request, string $base, mixed $paginator): array
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Web;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkService;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -13,7 +14,7 @@ class FeaturedArtworksController extends Controller
|
||||
{
|
||||
protected ArtworkService $artworks;
|
||||
|
||||
public function __construct(ArtworkService $artworks)
|
||||
public function __construct(ArtworkService $artworks, private readonly ArtworkMaturityService $maturity)
|
||||
{
|
||||
$this->artworks = $artworks;
|
||||
}
|
||||
@@ -29,7 +30,8 @@ class FeaturedArtworksController extends Controller
|
||||
/** @var LengthAwarePaginator $artworks */
|
||||
$artworks = $this->artworks->getFeaturedArtworks($typeFilter, $perPage);
|
||||
|
||||
$artworks->getCollection()->transform(function (Artwork $artwork) {
|
||||
$artworks->setCollection(
|
||||
collect($this->maturity->filterPayloadItems($artworks->getCollection()->map(function (Artwork $artwork): array {
|
||||
$primaryCategory = $artwork->categories->sortBy('sort_order')->first();
|
||||
$categoryName = $primaryCategory->name ?? '';
|
||||
$categorySlug = $primaryCategory->slug ?? '';
|
||||
@@ -37,7 +39,7 @@ class FeaturedArtworksController extends Controller
|
||||
$present = \App\Services\ThumbnailPresenter::present($artwork, 'md');
|
||||
$username = $artwork->user->username ?? $artwork->user->name ?? 'Skinbase';
|
||||
|
||||
return (object) [
|
||||
return $this->maturity->decoratePayload([
|
||||
'id' => $artwork->id,
|
||||
'name' => $artwork->title,
|
||||
'slug' => $artwork->slug,
|
||||
@@ -53,8 +55,11 @@ class FeaturedArtworksController extends Controller
|
||||
'height' => $artwork->height,
|
||||
'uname' => $artwork->user->name ?? 'Skinbase',
|
||||
'username' => $username,
|
||||
];
|
||||
});
|
||||
], $artwork, $request->user());
|
||||
})->values()->all(), $request->user()))
|
||||
->map(static fn (array $item): object => (object) $item)
|
||||
->values()
|
||||
);
|
||||
|
||||
$artworkTypes = [
|
||||
1 => 'Bronze Awards',
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ContentType;
|
||||
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -26,52 +26,6 @@ final class RssFeedController extends Controller
|
||||
/** Number of items per legacy feed. */
|
||||
private const FEED_LIMIT = 25;
|
||||
|
||||
/**
|
||||
* Grouped feed definitions shown on the /rss-feeds info page.
|
||||
* Each group has a 'label' and an array of 'feeds' with title + url.
|
||||
*/
|
||||
public const FEED_GROUPS = [
|
||||
'global' => [
|
||||
'label' => 'Global',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Artworks', 'url' => '/rss', 'description' => 'All new artworks across the platform.'],
|
||||
],
|
||||
],
|
||||
'discover' => [
|
||||
'label' => 'Discover',
|
||||
'feeds' => [
|
||||
['title' => 'Fresh Uploads', 'url' => '/rss/discover/fresh', 'description' => 'The newest artworks just published.'],
|
||||
['title' => 'Trending', 'url' => '/rss/discover/trending', 'description' => 'Most-viewed artworks over the past 7 days.'],
|
||||
['title' => 'Rising', 'url' => '/rss/discover/rising', 'description' => 'Artworks gaining momentum right now.'],
|
||||
],
|
||||
],
|
||||
'explore' => [
|
||||
'label' => 'Explore',
|
||||
'feeds' => [
|
||||
['title' => 'All Artworks', 'url' => '/rss/explore/artworks', 'description' => 'Latest artworks of all types.'],
|
||||
['title' => 'Wallpapers', 'url' => '/rss/explore/wallpapers', 'description' => 'Latest wallpapers.'],
|
||||
['title' => 'Skins', 'url' => '/rss/explore/skins', 'description' => 'Latest skins.'],
|
||||
['title' => 'Photography', 'url' => '/rss/explore/photography', 'description' => 'Latest photography.'],
|
||||
['title' => 'Trending Wallpapers', 'url' => '/rss/explore/wallpapers/trending', 'description' => 'Trending wallpapers this week.'],
|
||||
],
|
||||
],
|
||||
'blog' => [
|
||||
'label' => 'Blog',
|
||||
'feeds' => [
|
||||
['title' => 'Blog Posts', 'url' => '/rss/blog', 'description' => 'Latest posts from the Skinbase blog.'],
|
||||
],
|
||||
],
|
||||
'legacy' => [
|
||||
'label' => 'Legacy Feeds',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Uploads (XML)', 'url' => '/rss/latest-uploads.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Skins (XML)', 'url' => '/rss/latest-skins.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Wallpapers (XML)', 'url' => '/rss/latest-wallpapers.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Photos (XML)', 'url' => '/rss/latest-photos.xml', 'description' => 'Legacy XML feed.'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** Flat feed list kept for backward-compatibility (old view logic). */
|
||||
public const FEEDS = [
|
||||
'uploads' => ['title' => 'Latest Uploads', 'url' => '/rss/latest-uploads.xml'],
|
||||
@@ -80,6 +34,10 @@ final class RssFeedController extends Controller
|
||||
'photos' => ['title' => 'Latest Photos', 'url' => '/rss/latest-photos.xml'],
|
||||
];
|
||||
|
||||
public function __construct(private readonly ContentTypeSlugResolver $contentTypeResolver)
|
||||
{
|
||||
}
|
||||
|
||||
/** Info page at /rss-feeds */
|
||||
public function index(): View
|
||||
{
|
||||
@@ -94,7 +52,7 @@ final class RssFeedController extends Controller
|
||||
(object) ['name' => 'RSS Feeds', 'url' => '/rss-feeds'],
|
||||
]),
|
||||
'feeds' => self::FEEDS,
|
||||
'feed_groups' => self::FEED_GROUPS,
|
||||
'feed_groups' => $this->feedGroups(),
|
||||
'center_content' => true,
|
||||
'center_max' => '3xl',
|
||||
]);
|
||||
@@ -134,7 +92,7 @@ final class RssFeedController extends Controller
|
||||
|
||||
private function feedByContentType(string $slug, string $title, string $feedPath): Response
|
||||
{
|
||||
$contentType = ContentType::where('slug', $slug)->first();
|
||||
$contentType = $this->contentTypeResolver->resolve($slug)->contentType;
|
||||
|
||||
$query = Artwork::published()->with(['user'])->latest('published_at')->limit(self::FEED_LIMIT);
|
||||
|
||||
@@ -160,4 +118,70 @@ final class RssFeedController extends Controller
|
||||
'Content-Type' => 'application/rss+xml; charset=utf-8',
|
||||
]);
|
||||
}
|
||||
|
||||
private function feedGroups(): array
|
||||
{
|
||||
$exploreFeeds = [[
|
||||
'title' => 'All Artworks',
|
||||
'url' => '/rss/explore/artworks',
|
||||
'description' => 'Latest artworks of all types.',
|
||||
]];
|
||||
|
||||
foreach ($this->contentTypeResolver->publicContentTypes() as $contentType) {
|
||||
$name = (string) $contentType->name;
|
||||
$slug = (string) $contentType->slug;
|
||||
|
||||
$exploreFeeds[] = [
|
||||
'title' => $name,
|
||||
'url' => '/rss/explore/' . $slug,
|
||||
'description' => 'Latest ' . strtolower($name) . '.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->contentTypeResolver->publicContentTypes()->isNotEmpty()) {
|
||||
$firstType = $this->contentTypeResolver->publicContentTypes()->first();
|
||||
|
||||
$exploreFeeds[] = [
|
||||
'title' => 'Trending ' . $firstType->name,
|
||||
'url' => '/rss/explore/' . $firstType->slug . '/trending',
|
||||
'description' => 'Trending ' . strtolower((string) $firstType->name) . ' this week.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'global' => [
|
||||
'label' => 'Global',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Artworks', 'url' => '/rss', 'description' => 'All new artworks across the platform.'],
|
||||
],
|
||||
],
|
||||
'discover' => [
|
||||
'label' => 'Discover',
|
||||
'feeds' => [
|
||||
['title' => 'Fresh Uploads', 'url' => '/rss/discover/fresh', 'description' => 'The newest artworks just published.'],
|
||||
['title' => 'Trending', 'url' => '/rss/discover/trending', 'description' => 'Most-viewed artworks over the past 7 days.'],
|
||||
['title' => 'Rising', 'url' => '/rss/discover/rising', 'description' => 'Artworks gaining momentum right now.'],
|
||||
],
|
||||
],
|
||||
'explore' => [
|
||||
'label' => 'Explore',
|
||||
'feeds' => $exploreFeeds,
|
||||
],
|
||||
'blog' => [
|
||||
'label' => 'Blog',
|
||||
'feeds' => [
|
||||
['title' => 'Blog Posts', 'url' => '/rss/blog', 'description' => 'Latest posts from the Skinbase blog.'],
|
||||
],
|
||||
],
|
||||
'legacy' => [
|
||||
'label' => 'Legacy Feeds',
|
||||
'feeds' => [
|
||||
['title' => 'Latest Uploads (XML)', 'url' => '/rss/latest-uploads.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Skins (XML)', 'url' => '/rss/latest-skins.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Wallpapers (XML)', 'url' => '/rss/latest-wallpapers.xml', 'description' => 'Legacy XML feed.'],
|
||||
['title' => 'Latest Photos (XML)', 'url' => '/rss/latest-photos.xml', 'description' => 'Legacy XML feed.'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ final class SearchController extends Controller
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
$sort = $request->query('sort', 'latest');
|
||||
$hasQuery = $q !== '';
|
||||
|
||||
$sortMap = [
|
||||
'popular' => 'views:desc',
|
||||
@@ -30,17 +31,17 @@ final class SearchController extends Controller
|
||||
'downloads' => 'downloads:desc',
|
||||
];
|
||||
|
||||
$artworks = $q !== ''
|
||||
$artworks = $hasQuery
|
||||
? $this->search->search($q, [
|
||||
'sort' => ($sortMap[$sort] ?? 'created_at:desc'),
|
||||
])
|
||||
: $this->search->popular(24);
|
||||
|
||||
$groups = $q !== ''
|
||||
$groups = $hasQuery
|
||||
? $this->groups->searchCards($q, $request->user(), 6)
|
||||
: $this->groups->surfaceCards($request->user(), 'featured', 4);
|
||||
|
||||
$news = $q !== ''
|
||||
$news = $hasQuery
|
||||
? NewsArticle::query()
|
||||
->with(['author:id,username,name', 'category:id,name,slug'])
|
||||
->published()
|
||||
@@ -55,15 +56,59 @@ final class SearchController extends Controller
|
||||
->get()
|
||||
: collect();
|
||||
|
||||
$groupResults = collect($groups ?? []);
|
||||
$newsResults = collect($news ?? []);
|
||||
$resultCount = method_exists($artworks, 'total') ? (int) $artworks->total() : 0;
|
||||
$groupResultCount = $groupResults->count();
|
||||
$newsResultCount = $newsResults->count();
|
||||
$hasAnyResults = $resultCount > 0 || $groupResultCount > 0 || $newsResultCount > 0;
|
||||
$galleryArtworks = collect(method_exists($artworks, 'items') ? $artworks->items() : $artworks)
|
||||
->map(fn ($art) => $this->mapArtworkCard($art))
|
||||
->values();
|
||||
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
|
||||
|
||||
return view('search.index', [
|
||||
'q' => $q,
|
||||
'hasQuery' => $hasQuery,
|
||||
'sort' => $sort,
|
||||
'groups' => $groups,
|
||||
'groupResults' => $groupResults,
|
||||
'groupResultCount' => $groupResultCount,
|
||||
'artworks' => $artworks,
|
||||
'resultCount' => $resultCount,
|
||||
'news' => $news,
|
||||
'page_title' => $q !== '' ? 'Search: ' . $q . ' — Skinbase' : 'Search — Skinbase',
|
||||
'newsResults' => $newsResults,
|
||||
'newsResultCount' => $newsResultCount,
|
||||
'hasAnyResults' => $hasAnyResults,
|
||||
'galleryArtworks' => $galleryArtworks,
|
||||
'galleryNextPageUrl' => $galleryNextPageUrl,
|
||||
'page_title' => $hasQuery ? 'Search: ' . $q . ' — Skinbase' : 'Search — Skinbase',
|
||||
'page_meta_description' => 'Search Skinbase for artworks, creators, groups, photography, wallpapers and skins.',
|
||||
'page_robots' => 'noindex,follow',
|
||||
]);
|
||||
}
|
||||
|
||||
private function mapArtworkCard(mixed $artwork): array
|
||||
{
|
||||
return [
|
||||
'id' => $artwork->id ?? null,
|
||||
'name' => $artwork->name ?? null,
|
||||
'thumb' => $artwork->thumb_url ?? $artwork->thumb ?? null,
|
||||
'thumb_srcset' => $artwork->thumb_srcset ?? null,
|
||||
'uname' => $artwork->uname ?? '',
|
||||
'username' => $artwork->username ?? '',
|
||||
'avatar_url' => $artwork->avatar_url ?? null,
|
||||
'profile_url' => $artwork->profile_url ?? null,
|
||||
'published_as_type' => $artwork->published_as_type ?? null,
|
||||
'publisher' => $artwork->publisher ?? null,
|
||||
'category_name' => $artwork->category_name ?? '',
|
||||
'category_slug' => $artwork->category_slug ?? '',
|
||||
'slug' => $artwork->slug ?? '',
|
||||
'width' => $artwork->width ?? null,
|
||||
'height' => $artwork->height ?? null,
|
||||
'views' => $artwork->views ?? null,
|
||||
'likes' => $artwork->likes ?? null,
|
||||
'downloads' => $artwork->downloads ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\Recommendations\HybridSimilarArtworksService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Services\Vision\VectorService;
|
||||
@@ -35,6 +36,7 @@ final class SimilarArtworksPageController extends Controller
|
||||
|
||||
public function __construct(
|
||||
private readonly VectorService $vectors,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
private readonly HybridSimilarArtworksService $hybridService,
|
||||
) {}
|
||||
|
||||
@@ -70,6 +72,7 @@ final class SimilarArtworksPageController extends Controller
|
||||
'thumb_srcset' => $sourceMd['srcset'] ?? $sourceMd['url'] ?? null,
|
||||
'author_name' => $source->user?->name ?? 'Artist',
|
||||
'author_username' => $source->user?->username ?? '',
|
||||
'author_profile_url'=> $source->user?->username ? '/@' . $source->user->username : null,
|
||||
'author_avatar' => AvatarUrl::forUser(
|
||||
(int) ($source->user_id ?? 0),
|
||||
$source->user?->profile?->avatar_hash ?? null,
|
||||
@@ -79,6 +82,7 @@ final class SimilarArtworksPageController extends Controller
|
||||
'category_slug' => $primaryCat?->slug ?? '',
|
||||
'content_type_name' => $primaryCat?->contentType?->name ?? '',
|
||||
'content_type_slug' => $primaryCat?->contentType?->slug ?? '',
|
||||
'browse_url' => $primaryCat?->contentType?->slug ? url('/' . $primaryCat->contentType->slug) : url('/explore'),
|
||||
'tag_slugs' => $source->tags->pluck('slug')->take(5)->all(),
|
||||
'width' => $source->width ?? null,
|
||||
'height' => $source->height ?? null,
|
||||
@@ -144,8 +148,11 @@ final class SimilarArtworksPageController extends Controller
|
||||
'slug' => $art->slug ?? '',
|
||||
'width' => $art->width ?? null,
|
||||
'height' => $art->height ?? null,
|
||||
'maturity' => $art->maturity ?? null,
|
||||
])->values();
|
||||
|
||||
$galleryItems = collect($this->maturity->filterPayloadItems($galleryItems->all(), $request->user()))->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $galleryItems,
|
||||
'similarity_source' => $similaritySource,
|
||||
@@ -303,7 +310,7 @@ final class SimilarArtworksPageController extends Controller
|
||||
$username = $isGroupPublisher ? '' : ($artwork->user?->username ?? '');
|
||||
$profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null);
|
||||
|
||||
return (object) [
|
||||
return (object) $this->maturity->decoratePayload([
|
||||
'id' => $artwork->id,
|
||||
'name' => $artwork->title,
|
||||
'content_type_name' => $primary?->contentType?->name ?? '',
|
||||
@@ -328,6 +335,6 @@ final class SimilarArtworksPageController extends Controller
|
||||
'slug' => $artwork->slug ?? '',
|
||||
'width' => $artwork->width ?? null,
|
||||
'height' => $artwork->height ?? null,
|
||||
];
|
||||
], $artwork, request()->user());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\ContentType;
|
||||
use App\Models\Tag;
|
||||
use App\Services\ArtworkSearchService;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\Tags\TagDiscoveryService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -17,6 +18,7 @@ final class TagController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArtworkSearchService $search,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
private readonly TagDiscoveryService $tagDiscovery,
|
||||
) {}
|
||||
|
||||
@@ -61,12 +63,12 @@ final class TagController extends Controller
|
||||
]);
|
||||
|
||||
// Map artworks into the lightweight shape expected by the gallery React component.
|
||||
$galleryCollection = $artworks->getCollection()->map(function ($a) {
|
||||
$galleryCollection = collect($this->maturity->filterPayloadItems($artworks->getCollection()->map(function ($a) use ($request): array {
|
||||
$primaryCategory = $a->categories->sortBy('sort_order')->first();
|
||||
$present = ThumbnailPresenter::present($a, 'md');
|
||||
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($a->user_id ?? 0), $a->user?->profile?->avatar_hash ?? null, 64);
|
||||
|
||||
return (object) [
|
||||
return $this->maturity->decoratePayload([
|
||||
'id' => $a->id,
|
||||
'name' => $a->title ?? ($a->name ?? null),
|
||||
'content_type_name' => $primaryCategory?->contentType?->name ?? '',
|
||||
@@ -82,8 +84,10 @@ final class TagController extends Controller
|
||||
'width' => $a->width ?? null,
|
||||
'height' => $a->height ?? null,
|
||||
'slug' => $a->slug ?? null,
|
||||
];
|
||||
})->values();
|
||||
], $a, $request->user());
|
||||
})->values()->all(), $request->user()))
|
||||
->map(static fn (array $item): object => (object) $item)
|
||||
->values();
|
||||
|
||||
// Replace paginator collection with the gallery-shaped collection so
|
||||
// the gallery.index blade will generate the expected JSON payload.
|
||||
|
||||
Reference in New Issue
Block a user