Allow artworks to have one primary category and extra secondaries.
Keep artwork_category as membership, store a deterministic primary_category_id, and let upload plus studio pick up to five additional same-type categories without changing canonical URLs.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
@@ -7,17 +8,19 @@ use App\Enums\ReactionType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ArtworkResource;
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Models\Category;
|
||||
use App\Services\ArtworkNavigationService;
|
||||
use App\Services\ArtworkRelatedService;
|
||||
use App\Services\ErrorSuggestionService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Maturity\ArtworkMaturityService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Support\Seo\SeoFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Inertia\Inertia;
|
||||
@@ -29,6 +32,7 @@ final class ArtworkPageController extends Controller
|
||||
private readonly GroupService $groups,
|
||||
private readonly ArtworkMaturityService $maturity,
|
||||
private readonly ArtworkNavigationService $navigation,
|
||||
private readonly ArtworkRelatedService $relatedArtworks,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, int $id, ?string $slug = null): View|RedirectResponse|Response|InertiaResponse
|
||||
@@ -39,6 +43,7 @@ final class ArtworkPageController extends Controller
|
||||
if (! $raw) {
|
||||
// Artwork never existed → contextual 404
|
||||
$suggestions = app(ErrorSuggestionService::class);
|
||||
|
||||
return response(view('errors.contextual.artwork-not-found', [
|
||||
'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()),
|
||||
]), 404);
|
||||
@@ -73,24 +78,25 @@ final class ArtworkPageController extends Controller
|
||||
->limit(6)
|
||||
->get()
|
||||
->map(function (Artwork $a) {
|
||||
$slug = \Illuminate\Support\Str::slug((string) ($a->slug ?: $a->title)) ?: (string) $a->id;
|
||||
$md = \App\Services\ThumbnailPresenter::present($a, 'md');
|
||||
$slug = Str::slug((string) ($a->slug ?: $a->title)) ?: (string) $a->id;
|
||||
$md = ThumbnailPresenter::present($a, 'md');
|
||||
|
||||
return [
|
||||
'id' => $a->id,
|
||||
'title' => html_entity_decode((string) $a->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'id' => $a->id,
|
||||
'title' => html_entity_decode((string) $a->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'author' => html_entity_decode((string) ($a->user?->name ?: $a->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'url' => route('art.show', ['id' => $a->id, 'slug' => $slug]),
|
||||
'thumb' => $md['url'] ?? null,
|
||||
'url' => route('art.show', ['id' => $a->id, 'slug' => $slug]),
|
||||
'thumb' => $md['url'] ?? null,
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return response(view('errors.contextual.artwork-not-found', [
|
||||
'message' => 'This artwork is not publicly available.',
|
||||
'isForbidden' => true,
|
||||
'creatorArtworks' => $creatorArtworks,
|
||||
'creatorUsername' => $creatorUsername,
|
||||
'message' => 'This artwork is not publicly available.',
|
||||
'isForbidden' => true,
|
||||
'creatorArtworks' => $creatorArtworks,
|
||||
'creatorUsername' => $creatorUsername,
|
||||
'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()),
|
||||
]), 403);
|
||||
}
|
||||
@@ -101,9 +107,15 @@ final class ArtworkPageController extends Controller
|
||||
// distinct responses. Reusing that model avoids a second identical
|
||||
// primary-key lookup on the public path.
|
||||
$artwork = $raw;
|
||||
$artwork->load(['user.profile', 'group.owner.profile', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats', 'awardStat']);
|
||||
$artwork->load(['user.profile', 'group.owner.profile', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'categories.contentType', 'categories.parent.contentType', 'primaryCategory.contentType', 'primaryCategory.parent.contentType', 'tags', 'stats', 'awardStat']);
|
||||
|
||||
$this->loadCategoryAncestors($artwork->categories);
|
||||
$this->loadCategoryAncestors(
|
||||
$artwork->categories
|
||||
->concat(collect([$artwork->resolvedPrimaryCategory()]))
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values()
|
||||
);
|
||||
|
||||
$canonicalSlug = Str::slug((string) ($artwork->slug ?: $artwork->title));
|
||||
if ($canonicalSlug === '') {
|
||||
@@ -152,37 +164,7 @@ final class ArtworkPageController extends Controller
|
||||
'xl' => $thumbXl,
|
||||
], $canonical, $this->artworkBreadcrumbs($artwork, $canonical))->toArray();
|
||||
|
||||
$categoryIds = $artwork->categories->pluck('id')->filter()->values();
|
||||
$tagIds = $artwork->tags->pluck('id')->filter()->values();
|
||||
|
||||
$related = Artwork::query()
|
||||
->with(['user', 'group', 'categories.contentType'])
|
||||
->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);
|
||||
|
||||
if ($artwork->group_id) {
|
||||
$query->orWhere('group_id', $artwork->group_id);
|
||||
}
|
||||
|
||||
if ($categoryIds->isNotEmpty()) {
|
||||
$query->orWhereHas('categories', function ($categoryQuery) use ($categoryIds): void {
|
||||
$categoryQuery->whereIn('categories.id', $categoryIds->all());
|
||||
});
|
||||
}
|
||||
|
||||
if ($tagIds->isNotEmpty()) {
|
||||
$query->orWhereHas('tags', function ($tagQuery) use ($tagIds): void {
|
||||
$tagQuery->whereIn('tags.id', $tagIds->all());
|
||||
});
|
||||
}
|
||||
})
|
||||
->latest('published_at')
|
||||
->limit(12)
|
||||
->get()
|
||||
$related = $this->relatedArtworks->related($artwork, $request->user(), 12)
|
||||
->map(function (Artwork $item): array {
|
||||
$itemSlug = Str::slug((string) ($item->slug ?: $item->title));
|
||||
if ($itemSlug === '') {
|
||||
@@ -201,7 +183,7 @@ final class ArtworkPageController extends Controller
|
||||
'publisher_id' => $item->group ? (int) $item->group->id : (int) ($item->user?->id ?? 0),
|
||||
'url' => route('art.show', ['id' => $item->id, 'slug' => $itemSlug]),
|
||||
'thumb' => $sm['url'] ?? null,
|
||||
'thumb_srcset' => ($sm['url'] ?? '') . ' 320w, ' . ($md['url'] ?? '') . ' 640w',
|
||||
'thumb_srcset' => ($sm['url'] ?? '').' 320w, '.($md['url'] ?? '').' 640w',
|
||||
], $item, request()->user());
|
||||
})
|
||||
->values()
|
||||
@@ -210,20 +192,19 @@ final class ArtworkPageController extends Controller
|
||||
$canReadSession = $request->hasSession() && ! $request->attributes->get('skinbase.session_skipped');
|
||||
$viewerId = ($canReadSession && $request->user() !== null) ? (int) $request->user()->id : null;
|
||||
|
||||
|
||||
return Inertia::render('ArtworkPage', [
|
||||
'artwork' => $artworkData,
|
||||
'navigation' => $navigationData,
|
||||
'presentMd' => $thumbMd,
|
||||
'presentLg' => $thumbLg,
|
||||
'presentXl' => $thumbXl,
|
||||
'presentSq' => $thumbSq,
|
||||
'related' => $related,
|
||||
'canonicalUrl' => $canonical,
|
||||
'groupSummary' => $groupSummary,
|
||||
'artwork' => $artworkData,
|
||||
'navigation' => $navigationData,
|
||||
'presentMd' => $thumbMd,
|
||||
'presentLg' => $thumbLg,
|
||||
'presentXl' => $thumbXl,
|
||||
'presentSq' => $thumbSq,
|
||||
'related' => $related,
|
||||
'canonicalUrl' => $canonical,
|
||||
'groupSummary' => $groupSummary,
|
||||
'isAuthenticated' => $viewerId !== null,
|
||||
'reactionTotals' => $this->artworkReactionTotals((int) $artwork->id, $viewerId),
|
||||
'seo' => $seo,
|
||||
'reactionTotals' => $this->artworkReactionTotals((int) $artwork->id, $viewerId),
|
||||
'seo' => $seo,
|
||||
])->rootView('artworks.show');
|
||||
}
|
||||
|
||||
@@ -245,7 +226,7 @@ final class ArtworkPageController extends Controller
|
||||
|
||||
$totals = [];
|
||||
foreach (ReactionType::cases() as $type) {
|
||||
$slug = $type->value;
|
||||
$slug = $type->value;
|
||||
$count = (int) ($rows[$slug]->total ?? 0);
|
||||
|
||||
$mine = false;
|
||||
@@ -261,7 +242,7 @@ final class ArtworkPageController extends Controller
|
||||
'emoji' => $type->emoji(),
|
||||
'label' => $type->label(),
|
||||
'count' => $count,
|
||||
'mine' => $mine,
|
||||
'mine' => $mine,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -282,7 +263,7 @@ final class ArtworkPageController extends Controller
|
||||
->values();
|
||||
|
||||
if ($missingParentIds->isNotEmpty()) {
|
||||
$fetchedParents = \App\Models\Category::query()
|
||||
$fetchedParents = Category::query()
|
||||
->with('contentType')
|
||||
->whereIn('id', $missingParentIds->all())
|
||||
->get()
|
||||
@@ -308,12 +289,7 @@ final class ArtworkPageController extends Controller
|
||||
*/
|
||||
private function artworkBreadcrumbs(Artwork $artwork, string $canonical): array
|
||||
{
|
||||
$primaryCategory = $artwork->categories
|
||||
->sortBy(fn ($category) => [
|
||||
(int) ($category->sort_order ?? 0),
|
||||
(string) ($category->name ?? ''),
|
||||
])
|
||||
->first();
|
||||
$primaryCategory = $artwork->resolvedPrimaryCategory();
|
||||
|
||||
if ($primaryCategory === null) {
|
||||
return [
|
||||
@@ -338,7 +314,7 @@ final class ArtworkPageController extends Controller
|
||||
if ($contentTypeSlug !== '' && $contentTypeName !== '') {
|
||||
$breadcrumbs[] = [
|
||||
'name' => $contentTypeName,
|
||||
'url' => url('/' . $contentTypeSlug),
|
||||
'url' => url('/'.$contentTypeSlug),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -355,7 +331,7 @@ final class ArtworkPageController extends Controller
|
||||
$pathSegments[] = $slug;
|
||||
$breadcrumbs[] = [
|
||||
'name' => $name,
|
||||
'url' => url('/' . $contentTypeSlug . '/' . implode('/', $pathSegments)),
|
||||
'url' => url('/'.$contentTypeSlug.'/'.implode('/', $pathSegments)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user