157 lines
6.0 KiB
PHP
157 lines
6.0 KiB
PHP
<?php
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Web\BrowseGalleryController;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\ArtworkIndexRequest;
|
|
use App\Models\Artwork;
|
|
use App\Models\Category;
|
|
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\View\View;
|
|
|
|
class ArtworkController extends Controller
|
|
{
|
|
public function __construct(private readonly ContentTypeSlugResolver $contentTypeResolver)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Browse artworks with optional category filtering.
|
|
* Uses cursor pagination (no offset pagination) and only returns public, approved, not-deleted items.
|
|
*/
|
|
public function index(ArtworkIndexRequest $request, ?Category $category = null): View
|
|
{
|
|
$perPage = (int) ($request->get('per_page', 24));
|
|
|
|
$query = Artwork::public()->published();
|
|
|
|
if ($category) {
|
|
$query->whereHas('categories', function ($q) use ($category) {
|
|
$q->where('categories.id', $category->id);
|
|
});
|
|
}
|
|
|
|
if ($request->filled('q')) {
|
|
$q = $request->get('q');
|
|
$query->where(function ($sub) use ($q) {
|
|
$sub->where('title', 'like', '%' . $q . '%')
|
|
->orWhere('description', 'like', '%' . $q . '%');
|
|
});
|
|
}
|
|
|
|
$sort = $request->get('sort', 'latest');
|
|
if ($sort === 'oldest') {
|
|
$query->orderBy('published_at', 'asc');
|
|
} else {
|
|
$query->orderByDesc('published_at');
|
|
}
|
|
|
|
// Important: do NOT eager-load artwork_stats in listings
|
|
$artworks = $query->cursorPaginate($perPage);
|
|
|
|
return view('artworks.index', [
|
|
'artworks' => $artworks,
|
|
'category' => $category,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show a single artwork by slug. Resolve the slug manually to avoid implicit
|
|
* route-model binding exceptions when the slug does not correspond to an artwork.
|
|
*/
|
|
public function show(Request $request, string $contentTypeSlug, string $categoryPath, $artwork = null)
|
|
{
|
|
$resolution = $this->contentTypeResolver->resolve($contentTypeSlug);
|
|
if (! $resolution->found() || $resolution->contentType === null) {
|
|
abort(404);
|
|
}
|
|
|
|
$resolvedContentTypeSlug = strtolower((string) $resolution->contentType->slug);
|
|
|
|
if ($resolution->requiresRedirect()) {
|
|
return $this->redirectToCanonicalArtworkPath($request, $resolvedContentTypeSlug, $categoryPath, $artwork, 301);
|
|
}
|
|
|
|
// Manually resolve artwork by slug when provided. The route may bind
|
|
// the 'artwork' parameter to an Artwork model or pass the slug string.
|
|
$foundArtwork = null;
|
|
$artworkSlug = null;
|
|
if ($artwork instanceof Artwork) {
|
|
$foundArtwork = $artwork;
|
|
$artworkSlug = $artwork->slug;
|
|
} elseif ($artwork) {
|
|
$artworkSlug = (string) $artwork;
|
|
$foundArtwork = $this->findArtworkForCategoryPath($resolvedContentTypeSlug, $categoryPath, $artworkSlug);
|
|
}
|
|
|
|
// When the URL can represent a nested category path (e.g. /skins/audio/winamp),
|
|
// prefer category rendering over artwork slug collisions so same-level groups
|
|
// behave consistently.
|
|
if (! empty($artworkSlug)) {
|
|
$combinedPath = trim($categoryPath . '/' . $artworkSlug, '/');
|
|
$resolvedCategory = Category::findByPath($resolvedContentTypeSlug, $combinedPath);
|
|
if ($resolvedCategory) {
|
|
return app(BrowseGalleryController::class)->content(request(), $resolvedContentTypeSlug, $combinedPath);
|
|
}
|
|
}
|
|
|
|
// If no artwork was found, treat the request as a category path.
|
|
// The route places the artwork slug in the last segment, so include it.
|
|
// Delegate to BrowseGalleryController to render the same modern gallery
|
|
// layout used by routes like /skins/audio.
|
|
if (! $foundArtwork) {
|
|
$combinedPath = $categoryPath;
|
|
if ($artworkSlug) {
|
|
$combinedPath = trim($categoryPath . '/' . $artworkSlug, '/');
|
|
}
|
|
return app(BrowseGalleryController::class)->content(request(), $resolvedContentTypeSlug, $combinedPath);
|
|
}
|
|
|
|
if (! $foundArtwork->is_public || ! $foundArtwork->is_approved || $foundArtwork->trashed()) {
|
|
abort(404);
|
|
}
|
|
|
|
// Delegate to the canonical ArtworkPageController which builds all
|
|
// required view data ($meta, thumbnails, related items, comments, etc.)
|
|
return app(\App\Http\Controllers\Web\ArtworkPageController::class)->show(
|
|
$request,
|
|
(int) $foundArtwork->id,
|
|
$foundArtwork->slug,
|
|
);
|
|
}
|
|
|
|
private function findArtworkForCategoryPath(string $contentTypeSlug, string $categoryPath, string $artworkSlug): ?Artwork
|
|
{
|
|
$segments = array_values(array_filter(explode('/', trim($categoryPath, '/'))));
|
|
$category = Category::findByPath(strtolower($contentTypeSlug), $segments);
|
|
|
|
$query = Artwork::query()->where('slug', $artworkSlug);
|
|
|
|
if ($category) {
|
|
$query->whereHas('categories', function ($categoryQuery) use ($category): void {
|
|
$categoryQuery->where('categories.id', $category->id);
|
|
});
|
|
}
|
|
|
|
return $query
|
|
->orderByDesc('published_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
private function redirectToCanonicalArtworkPath(Request $request, string $contentTypeSlug, string $categoryPath, Artwork|string|null $artwork, int $status = 301): RedirectResponse
|
|
{
|
|
$artworkSlug = $artwork instanceof Artwork ? $artwork->slug : (string) $artwork;
|
|
$target = url('/' . trim($contentTypeSlug . '/' . trim($categoryPath, '/') . '/' . trim($artworkSlug, '/'), '/'));
|
|
$queryString = $request->getQueryString();
|
|
|
|
if ($queryString) {
|
|
$target .= '?' . $queryString;
|
|
}
|
|
|
|
return redirect()->to($target, $status);
|
|
}
|
|
}
|