feat: increase gallery grid from 4 to 5 columns per row on desktopfeat: increase gallery grid from 4 to 5 columns per row on desktop

This commit is contained in:
2026-02-25 19:11:23 +01:00
parent 5c97488e80
commit 0032aec02f
131 changed files with 15674 additions and 597 deletions

View File

@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Http\Resources\ArtworkResource;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Services\ThumbnailPresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -102,6 +103,27 @@ final class ArtworkPageController extends Controller
->values()
->all();
$comments = ArtworkComment::with(['user.profile'])
->where('artwork_id', $artwork->id)
->where('is_approved', true)
->orderBy('created_at')
->limit(500)
->get()
->map(fn(ArtworkComment $c) => [
'id' => $c->id,
'content' => (string) $c->content,
'created_at' => $c->created_at?->toIsoString(),
'user' => [
'id' => $c->user?->id,
'name' => $c->user?->name,
'username' => $c->user?->username,
'profile_url' => $c->user?->username ? '/@' . $c->user->username : null,
'avatar_url' => $c->user?->profile?->avatar_url,
],
])
->values()
->all();
return view('artworks.show', [
'artwork' => $artwork,
'artworkData' => $artworkData,
@@ -111,6 +133,7 @@ final class ArtworkPageController extends Controller
'presentSq' => $thumbSq,
'meta' => $meta,
'relatedItems' => $related,
'comments' => $comments,
]);
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\Category;
use App\Models\ContentType;
use App\Models\Artwork;
use App\Services\ArtworkService;
use App\Services\ArtworkSearchService;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Pagination\AbstractPaginator;
@@ -15,8 +16,17 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
{
private const CONTENT_TYPE_SLUGS = ['photography', 'wallpapers', 'skins', 'other'];
public function __construct(private ArtworkService $artworks)
{
private const SORT_MAP = [
'latest' => 'created_at:desc',
'popular' => 'views:desc',
'liked' => 'likes:desc',
'downloads' => 'downloads:desc',
];
public function __construct(
private ArtworkService $artworks,
private ArtworkSearchService $search,
) {
}
public function browse(Request $request)
@@ -24,7 +34,10 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
$sort = (string) $request->query('sort', 'latest');
$perPage = $this->resolvePerPage($request);
$artworks = $this->artworks->browsePublicArtworks($perPage, $sort);
$artworks = Artwork::search('')->options([
'filter' => 'is_public = true AND is_approved = true',
'sort' => [self::SORT_MAP[$sort] ?? 'created_at:desc'],
])->paginate($perPage);
$seo = $this->buildPaginationSeo($request, url('/browse'), $artworks);
$mainCategories = $this->mainCategories();
@@ -69,7 +82,10 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
$normalizedPath = trim((string) $path, '/');
if ($normalizedPath === '') {
$artworks = $this->artworks->getArtworksByContentType($contentSlug, $perPage, $sort);
$artworks = Artwork::search('')->options([
'filter' => 'is_public = true AND is_approved = true AND content_type = "' . $contentSlug . '"',
'sort' => [self::SORT_MAP[$sort] ?? 'created_at:desc'],
])->paginate($perPage);
$seo = $this->buildPaginationSeo($request, url('/' . $contentSlug), $artworks);
return view('gallery.index', [
@@ -98,7 +114,10 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller
abort(404);
}
$artworks = $this->artworks->getArtworksByCategoryPath(array_merge([$contentSlug], $segments), $perPage, $sort);
$artworks = Artwork::search('')->options([
'filter' => 'is_public = true AND is_approved = true AND category = "' . $category->slug . '"',
'sort' => [self::SORT_MAP[$sort] ?? 'created_at:desc'],
])->paginate($perPage);
$seo = $this->buildPaginationSeo($request, url('/' . $contentSlug . '/' . strtolower($category->full_slug_path)), $artworks);
$subcategories = $category->children()->orderBy('sort_order')->orderBy('name')->get();

View File

@@ -17,6 +17,11 @@ class CategoryController extends Controller
$this->artworkService = $artworkService;
}
public function index(Request $request)
{
return $this->browseCategories();
}
public function show(Request $request, $id, $slug = null, $group = null)
{
$path = trim($request->path(), '/');

View File

@@ -27,17 +27,20 @@ class HomeController extends Controller
$featuredResult = $this->artworks->getFeaturedArtworks(null, 39);
if ($featuredResult instanceof \Illuminate\Pagination\LengthAwarePaginator) {
$featured = $featuredResult->getCollection()->first();
$featuredCollection = $featuredResult->getCollection();
$featured = $featuredCollection->get(0);
$memberFeatured = $featuredCollection->get(1);
} elseif (is_array($featuredResult)) {
$featured = $featuredResult[0] ?? null;
$memberFeatured = $featuredResult[1] ?? null;
} elseif ($featuredResult instanceof Collection) {
$featured = $featuredResult->first();
$featured = $featuredResult->get(0);
$memberFeatured = $featuredResult->get(1);
} else {
$featured = $featuredResult;
$memberFeatured = null;
}
$memberFeatured = $featured;
$latestUploads = $this->artworks->getLatestArtworks(20);
// Forum news (prefer migrated legacy news category id 2876, fallback to slug)

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Services\ArtworkSearchService;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class SearchController extends Controller
{
public function __construct(private readonly ArtworkSearchService $search) {}
public function index(Request $request): View
{
$q = trim((string) $request->query('q', ''));
$sort = $request->query('sort', 'latest');
$sortMap = [
'popular' => 'views:desc',
'likes' => 'likes:desc',
'latest' => 'created_at:desc',
'downloads' => 'downloads:desc',
];
$artworks = null;
$popular = collect();
if ($q !== '') {
$artworks = $this->search->search($q, [
'sort' => ($sortMap[$sort] ?? 'created_at:desc'),
]);
} else {
$popular = $this->search->popular(16)->getCollection();
}
return view('search.index', [
'q' => $q,
'sort' => $sort,
'artworks' => $artworks ?? collect()->paginate(0),
'popular' => $popular,
'page_title' => $q !== '' ? 'Search: ' . $q . ' — Skinbase' : 'Search — Skinbase',
'page_meta_description' => 'Search Skinbase for artworks, photography, wallpapers and skins.',
'page_robots' => 'noindex,follow',
]);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\ContentType;
use Illuminate\Support\Facades\DB;
class SectionsController extends Controller
{
public function index()
{
// Load all content types with full category tree (roots + children)
$contentTypes = ContentType::with([
'rootCategories' => function ($q) {
$q->active()
->withCount(['artworks as artwork_count'])
->orderBy('sort_order')
->orderBy('name');
},
'rootCategories.children' => function ($q) {
$q->active()
->withCount(['artworks as artwork_count'])
->orderBy('sort_order')
->orderBy('name');
},
])->orderBy('id')->get();
// Total artwork counts per content type via a single aggregation query
$artworkCountsByType = DB::table('artworks')
->join('artwork_category', 'artworks.id', '=', 'artwork_category.artwork_id')
->join('categories', 'artwork_category.category_id', '=', 'categories.id')
->where('artworks.is_approved', true)
->where('artworks.is_public', true)
->whereNull('artworks.deleted_at')
->select('categories.content_type_id', DB::raw('COUNT(DISTINCT artworks.id) as total'))
->groupBy('categories.content_type_id')
->pluck('total', 'content_type_id');
return view('web.sections', [
'contentTypes' => $contentTypes,
'artworkCountsByType' => $artworkCountsByType,
'page_title' => 'Browse Sections',
'page_meta_description' => 'Browse all artwork sections on Skinbase — Photography, Wallpapers, Skins and more.',
]);
}
}

View File

@@ -6,24 +6,57 @@ namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use App\Services\ArtworkSearchService;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class TagController extends Controller
{
public function show(Tag $tag): View
public function __construct(private readonly ArtworkSearchService $search) {}
public function show(Tag $tag, Request $request): View
{
$artworks = $tag->artworks()
->public()
->published()
->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id')
->orderByDesc('artwork_stats.views')
->orderByDesc('artworks.published_at')
->select('artworks.*')
->paginate(24);
$sort = $request->query('sort', 'popular'); // popular | latest | downloads
$perPage = min((int) $request->query('per_page', 24), 100);
// Convert sort param to Meili sort expression
$sortMap = [
'popular' => 'views:desc',
'likes' => 'likes:desc',
'latest' => 'created_at:desc',
'downloads' => 'downloads:desc',
];
$meiliSort = $sortMap[$sort] ?? 'views:desc';
$artworks = \App\Models\Artwork::search('')
->options([
'filter' => 'is_public = true AND is_approved = true AND tags = "' . addslashes($tag->slug) . '"',
'sort' => [$meiliSort],
])
->paginate($perPage)
->appends(['sort' => $sort]);
// Eager-load relations needed by the artwork-card component.
// Scout returns bare Eloquent models; without this, each card triggers N+1 queries.
$artworks->getCollection()->loadMissing(['user.profile']);
// OG image: first result's thumbnail
$ogImage = null;
if ($artworks->count() > 0) {
$first = $artworks->getCollection()->first();
$ogImage = $first?->thumbUrl('md');
}
return view('tags.show', [
'tag' => $tag,
'tag' => $tag,
'artworks' => $artworks,
'sort' => $sort,
'ogImage' => $ogImage,
'page_title' => 'Artworks tagged "' . $tag->name . '" — Skinbase',
'page_meta_description' => 'Browse all Skinbase artworks tagged "' . $tag->name . '". Discover photography, wallpapers and skins.',
'page_canonical' => route('tags.show', $tag->slug),
'page_robots' => 'index,follow',
]);
}
}