diff --git a/.gitignore b/.gitignore
index a147aee2..a1213f63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
/public/build
/public/hot
/public/storage
+/public/files
/storage/*.key
/storage/pail
/vendor
diff --git a/app/Http/Controllers/BrowseGalleryController.php b/app/Http/Controllers/BrowseGalleryController.php
new file mode 100644
index 00000000..78509a23
--- /dev/null
+++ b/app/Http/Controllers/BrowseGalleryController.php
@@ -0,0 +1,181 @@
+query('sort', 'latest');
+ $perPage = $this->resolvePerPage($request);
+
+ $artworks = $this->artworks->browsePublicArtworks($perPage, $sort);
+
+ $mainCategories = $this->mainCategories();
+
+ return view('gallery.index', [
+ 'gallery_type' => 'browse',
+ 'mainCategories' => $mainCategories,
+ 'subcategories' => $mainCategories,
+ 'contentType' => null,
+ 'category' => null,
+ 'artworks' => $artworks,
+ 'hero_title' => 'Browse Artworks',
+ 'hero_description' => 'List of all uploaded artworks across Skins, Wallpapers, Photography, and Other.',
+ 'breadcrumbs' => collect(),
+ 'page_title' => 'Browse Uploaded Artworks - Photography, Wallpapers and Skins at SkinBase',
+ 'page_meta_description' => "Browse Uploaded Photography, Wallpapers and Skins to one of the world's oldest online social community for artists and art enthusiasts.",
+ 'page_meta_keywords' => 'photography, wallpapers, skins, stock, browse, social, community, artist, picture, photo',
+ 'page_canonical' => url('/browse'),
+ ]);
+ }
+
+ public function content(Request $request, string $contentTypeSlug, ?string $path = null)
+ {
+ $contentSlug = strtolower($contentTypeSlug);
+ if (! in_array($contentSlug, self::CONTENT_TYPE_SLUGS, true)) {
+ abort(404);
+ }
+
+ $contentType = ContentType::where('slug', $contentSlug)->first();
+ if (! $contentType) {
+ abort(404);
+ }
+
+ $sort = (string) $request->query('sort', 'latest');
+ $perPage = $this->resolvePerPage($request);
+
+ $mainCategories = $this->mainCategories();
+ $rootCategories = $contentType->rootCategories()->orderBy('sort_order')->orderBy('name')->get();
+
+ $normalizedPath = trim((string) $path, '/');
+ if ($normalizedPath === '') {
+ $artworks = $this->artworks->getArtworksByContentType($contentSlug, $perPage, $sort);
+
+ return view('gallery.index', [
+ 'gallery_type' => 'content-type',
+ 'mainCategories' => $mainCategories,
+ 'subcategories' => $rootCategories,
+ 'contentType' => $contentType,
+ 'category' => null,
+ 'artworks' => $artworks,
+ 'hero_title' => $contentType->name,
+ 'hero_description' => $contentType->description ?? ($contentType->name . ' artworks on Skinbase.'),
+ 'breadcrumbs' => collect([(object) ['name' => $contentType->name, 'url' => '/' . $contentSlug]]),
+ 'page_title' => $contentType->name,
+ 'page_meta_description' => $contentType->description ?? ($contentType->name . ' artworks on Skinbase'),
+ 'page_meta_keywords' => strtolower($contentType->slug) . ', skinbase, artworks, wallpapers, skins, photography',
+ 'page_canonical' => url('/' . $contentSlug),
+ ]);
+ }
+
+ $segments = array_values(array_filter(explode('/', $normalizedPath)));
+ $category = Category::findByPath($contentSlug, $segments);
+ if (! $category) {
+ abort(404);
+ }
+
+ $artworks = $this->artworks->getArtworksByCategoryPath(array_merge([$contentSlug], $segments), $perPage, $sort);
+
+ $subcategories = $category->children()->orderBy('sort_order')->orderBy('name')->get();
+ if ($subcategories->isEmpty()) {
+ $subcategories = $rootCategories;
+ }
+
+ $breadcrumbs = collect($category->breadcrumbs)
+ ->map(function (Category $crumb) {
+ return (object) [
+ 'name' => $crumb->name,
+ 'url' => $crumb->url,
+ ];
+ });
+
+ return view('gallery.index', [
+ 'gallery_type' => 'category',
+ 'mainCategories' => $mainCategories,
+ 'subcategories' => $subcategories,
+ 'contentType' => $contentType,
+ 'category' => $category,
+ 'artworks' => $artworks,
+ 'hero_title' => $category->name,
+ 'hero_description' => $category->description ?? ($contentType->name . ' artworks on Skinbase.'),
+ 'breadcrumbs' => $breadcrumbs,
+ 'page_title' => $category->name,
+ 'page_meta_description' => $category->description ?? ($contentType->name . ' artworks on Skinbase'),
+ 'page_meta_keywords' => strtolower($contentType->slug) . ', skinbase, artworks, wallpapers, skins, photography',
+ 'page_canonical' => url('/' . $contentSlug . '/' . strtolower($category->full_slug_path)),
+ ]);
+ }
+
+ public function showArtwork(Request $request, string $contentTypeSlug, string $categoryPath, string $artwork)
+ {
+ return app(ArtworkController::class)->show(
+ $request,
+ strtolower($contentTypeSlug),
+ trim($categoryPath, '/'),
+ $artwork
+ );
+ }
+
+ public function legacyCategory(Request $request, ?string $group = null, ?string $slug = null, ?string $id = null)
+ {
+ if ($id !== null && ctype_digit((string) $id)) {
+ $category = Category::with('contentType')->find((int) $id);
+ if (! $category || ! $category->contentType) {
+ abort(404);
+ }
+
+ return redirect($category->url, 301);
+ }
+
+ $contentSlug = strtolower((string) $group);
+ if (! in_array($contentSlug, self::CONTENT_TYPE_SLUGS, true)) {
+ abort(404);
+ }
+
+ $target = '/' . $contentSlug;
+ $normalizedSlug = trim((string) $slug, '/');
+ if ($normalizedSlug !== '') {
+ $target .= '/' . strtolower($normalizedSlug);
+ }
+
+ if ($request->query()) {
+ $target .= '?' . http_build_query($request->query());
+ }
+
+ return redirect($target, 301);
+ }
+
+ private function resolvePerPage(Request $request): int
+ {
+ $value = (int) $request->query('per_page', 40);
+
+ return max(12, min($value, 80));
+ }
+
+ private function mainCategories(): Collection
+ {
+ return ContentType::orderBy('id')
+ ->get(['name', 'slug'])
+ ->map(function (ContentType $type) {
+ return (object) [
+ 'id' => $type->id,
+ 'name' => $type->name,
+ 'slug' => $type->slug,
+ 'url' => '/' . strtolower($type->slug),
+ ];
+ });
+ }
+}
diff --git a/resources/views/legacy/browse.blade.php b/resources/views/gallery/index.blade.php
similarity index 67%
rename from resources/views/legacy/browse.blade.php
rename to resources/views/gallery/index.blade.php
index d1cd50a5..396d623b 100644
--- a/resources/views/legacy/browse.blade.php
+++ b/resources/views/gallery/index.blade.php
@@ -24,20 +24,31 @@
Main Categories:
Browse Subcategories:
@@ -48,21 +59,34 @@
-
Browse
+
+ @if(($gallery_type ?? null) === 'browse')
+ Browse
+ @elseif(isset($contentType) && $contentType)
+
{{ $contentType->name }}
+ @if(($gallery_type ?? null) === 'category')
+ @foreach($breadcrumbs as $crumb)
+
›
+
{{ $crumb->name }}
+ @endforeach
+ @endif
+ @endif
+
-
Browse Artworks
+
{{ $hero_title ?? 'Browse Artworks' }}
-
All categories
-
List of all uploaded artworks across Skins, Wallpapers, Photography, and Other.
+
{{ $hero_title ?? 'Browse Artworks' }}
+
{!! $hero_description ?? '' !!}
+
-
+
@forelse ($artworks as $art)
@include('legacy._artwork_card', ['art' => $art])
@@ -77,7 +101,9 @@
- {{ $artworks->withQueryString()->links() }}
+ @if ($artworks instanceof \Illuminate\Contracts\Pagination\Paginator)
+ {{ method_exists($artworks, 'withQueryString') ? $artworks->withQueryString()->links() : $artworks->links() }}
+ @endif
@@ -90,9 +116,6 @@
@push('styles')
@endpush
diff --git a/resources/views/legacy/_artwork_card.blade.php b/resources/views/legacy/_artwork_card.blade.php
index d0c37cf5..1d7d1fa7 100644
--- a/resources/views/legacy/_artwork_card.blade.php
+++ b/resources/views/legacy/_artwork_card.blade.php
@@ -17,16 +17,48 @@
$category = trim((string) ($art->category_name ?? $art->category ?? 'General'));
$license = trim((string) ($art->license ?? 'Standard'));
$resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : '')));
- $likes = (int) ($art->likes ?? $art->favourites ?? 0);
- $downloads = (int) ($art->downloads ?? $art->downloaded ?? 0);
+ // Safe integer extractor: handle numeric, arrays, Collections, or relations
+ $safeInt = function ($v, $fallback = 0) {
+ if (is_numeric($v)) return (int) $v;
+ if (is_array($v)) return count($v);
+ if (is_object($v)) {
+ // Eloquent Collections implement Countable and have count()
+ if (method_exists($v, 'count')) return (int) $v->count();
+ if ($v instanceof Countable) return (int) count($v);
+ }
+ return (int) $fallback;
+ };
+
+ $likes = $safeInt($art->likes ?? $art->favourites ?? 0);
+ $downloads = $safeInt($art->downloads ?? $art->downloaded ?? 0);
$img_src = (string) ($art->thumb ?? $art->thumbnail_url ?? '/images/placeholder.jpg');
$img_srcset = (string) ($art->thumb_srcset ?? $art->thumbnail_srcset ?? $img_src);
$img_avif_srcset = (string) ($art->thumb_avif_srcset ?? $img_srcset);
$img_webp_srcset = (string) ($art->thumb_webp_srcset ?? $img_srcset);
- $img_width = max(1, (int) ($art->width ?? 800));
- $img_height = max(1, (int) ($art->height ?? 600));
+ // Width/height may be stored directly or as a related object; handle safely
+ $resolveDimension = function ($val, $fallback) {
+ if (is_numeric($val)) return (int) $val;
+ if (is_array($val)) {
+ $v = reset($val);
+ return is_numeric($v) ? (int) $v : (int) $fallback;
+ }
+ if (is_object($val)) {
+ if (method_exists($val, 'first')) {
+ $f = $val->first();
+ if (is_object($f) && isset($f->width)) return (int) ($f->width ?: $fallback);
+ if (is_object($f) && isset($f->height)) return (int) ($f->height ?: $fallback);
+ }
+ // Try numeric cast otherwise
+ if (isset($val->width)) return (int) $val->width;
+ if (isset($val->height)) return (int) $val->height;
+ }
+ return (int) $fallback;
+ };
+
+ $img_width = max(1, $resolveDimension($art->width ?? null, 800));
+ $img_height = max(1, $resolveDimension($art->height ?? null, 600));
$contentUrl = $img_src;
$cardUrl = (string) ($art->url ?? '#');
diff --git a/resources/views/legacy/category-slug.blade.php b/resources/views/legacy/category-slug.blade.php
deleted file mode 100644
index ff9f492d..00000000
--- a/resources/views/legacy/category-slug.blade.php
+++ /dev/null
@@ -1,148 +0,0 @@
-@extends('layouts.nova')
-
-@php
- use App\Banner;
-@endphp
-
-@section('content')
-
- @php Banner::ShowResponsiveAd(); @endphp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ $contentType->name }}
- @foreach ($category->breadcrumbs as $crumb)
-
› {{ $crumb->name }}
- @endforeach
-
-
- @php
- // Use the current (last) breadcrumb as the header so
- // the page shows the leaf category name (e.g. "Winamp").
- $breadcrumbs = is_array($category->breadcrumbs) ? $category->breadcrumbs : [$category];
- $headerCategory = end($breadcrumbs) ?: $category;
- @endphp
-
-
{{ $headerCategory->name }}
-
-
-
-
{{ $headerCategory->name }}
-
{!! $headerCategory->description ?? ($contentType->name . ' artworks on Skinbase.') !!}
-
-
- {{-- soft fade at bottom of hero to blend into main background --}}
-
-
-
-
-
-
-
- @forelse ($artworks as $art)
- @include('legacy._artwork_card', ['art' => $art])
- @empty
-
-
No Artworks Yet
-
-
Once uploads arrive they will appear here. Check back soon.
-
-
- @endforelse
-
-
-
- @if ($artworks instanceof \Illuminate\Contracts\Pagination\Paginator)
- {{ $artworks->links() }}
- @endif
-
-
-
-
-
-
-
-
-@endsection
-
-@push('styles')
-
-@endpush
-
-@push('scripts')
-
-@endpush
diff --git a/resources/views/legacy/content-type.blade.php b/resources/views/legacy/content-type.blade.php
deleted file mode 100644
index c1fc897a..00000000
--- a/resources/views/legacy/content-type.blade.php
+++ /dev/null
@@ -1,158 +0,0 @@
-@extends('layouts.nova')
-
-@php
- use App\Banner;
-@endphp
-
-@section('content')
-
- @php Banner::ShowResponsiveAd(); @endphp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ $contentType->name }}
-
-
-
-
{{ $contentType->name }}
-
{!! $page_meta_description ?? ($contentType->name . ' artworks on Skinbase.') !!}
-
-
- {{-- soft fade at bottom of hero to blend into main background --}}
-
-
-
-
-
-
-
- @forelse ($artworks as $art)
- @include('legacy._artwork_card', ['art' => $art])
- @empty
-
-
No Artworks Yet
-
-
Once uploads arrive they will appear here. Check back soon.
-
-
- @endforelse
-
-
-
- {{ $artworks->withQueryString()->links('pagination::tailwind') }}
-
-
-
-
-
-
-
-
-@endsection
-
-@push('styles')
-
-@endpush
-
-@push('scripts')
-
-@endpush
diff --git a/resources/views/legacy/photography.blade.php b/resources/views/legacy/photography.blade.php
deleted file mode 100644
index 6296018a..00000000
--- a/resources/views/legacy/photography.blade.php
+++ /dev/null
@@ -1,153 +0,0 @@
-@extends('layouts.nova')
-
-@section('content')
-
- @php \App\Banner::ShowResponsiveAd(); @endphp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @foreach($artworks as $art)
- @include('legacy._artwork_card', ['art' => $art])
- @endforeach
-
-
-
-
-
-
-
-
-@endsection
-
-@push('styles')
-
-@endpush
-
-@push('scripts')
-
-@endpush
diff --git a/routes/legacy.php b/routes/legacy.php
index 6c9d9309..15384dd4 100644
--- a/routes/legacy.php
+++ b/routes/legacy.php
@@ -7,7 +7,6 @@ use App\Http\Controllers\Legacy\AvatarController;
use App\Http\Controllers\Legacy\ForumController;
use App\Http\Controllers\Legacy\NewsController;
use App\Http\Controllers\Legacy\CategoryController;
-use App\Http\Controllers\Legacy\BrowseController;
use App\Http\Controllers\Legacy\FeaturedArtworksController;
use App\Http\Controllers\Legacy\DailyUploadsController;
use App\Http\Controllers\Legacy\ChatController;
@@ -27,7 +26,7 @@ use App\Http\Controllers\BrowseCategoriesController;
use App\Http\Controllers\GalleryController;
use App\Http\Controllers\Legacy\ReceivedCommentsController;
use App\Http\Controllers\Legacy\UserController as LegacyUserController;
-use App\Http\Controllers\Legacy\PhotographyController;
+use App\Http\Controllers\BrowseGalleryController;
// Legacy site routes
Route::get('/', [HomeController::class, 'index'])->name('legacy.home');
@@ -44,9 +43,9 @@ Route::get('/forum/{topic_id}/{slug?}', [ForumController::class, 'topic'])->wher
Route::get('/news/{id}/{slug?}', [NewsController::class, 'show'])->where('id', '\\d+')->name('legacy.news.show');
Route::get('/categories', [CategoryController::class, 'index'])->name('legacy.categories');
-Route::get('/category/{group}/{slug?}/{id?}', [CategoryController::class, 'show'])->name('legacy.category');
+Route::get('/category/{group}/{slug?}/{id?}', [BrowseGalleryController::class, 'legacyCategory'])->name('legacy.category');
-Route::get('/browse', [BrowseController::class, 'index'])->name('legacy.browse');
+Route::get('/browse', [BrowseGalleryController::class, 'browse'])->name('legacy.browse');
Route::get('/featured', [FeaturedArtworksController::class, 'index'])->name('legacy.featured');
Route::get('/featured-artworks', [FeaturedArtworksController::class, 'index'])->name('legacy.featured_artworks');
Route::get('/daily-uploads', [DailyUploadsController::class, 'index'])->name('legacy.daily_uploads');
diff --git a/routes/web.php b/routes/web.php
index 013a97c3..925767c0 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -115,12 +115,12 @@ Route::bind('artwork', function ($value) {
// to serve photography's root page. This catch-all route delegates to a controller that
// will forward to the appropriate existing controller (artwork or category handlers).
// Provide a named route alias for legacy artwork URL generation used in tests.
-Route::get('/{contentTypeSlug}/{categoryPath}/{artwork}', [\App\Http\Controllers\ContentRouterController::class, 'handle'])
+Route::get('/{contentTypeSlug}/{categoryPath}/{artwork}', [\App\Http\Controllers\BrowseGalleryController::class, 'showArtwork'])
->where('contentTypeSlug', 'photography|wallpapers|skins|other')
->where('categoryPath', '[^/]+(?:/[^/]+)*')
->name('artworks.show');
-Route::get('/{contentTypeSlug}/{path?}', [\App\Http\Controllers\ContentRouterController::class, 'handle'])
+Route::get('/{contentTypeSlug}/{path?}', [\App\Http\Controllers\BrowseGalleryController::class, 'content'])
->where('contentTypeSlug', 'photography|wallpapers|skins|other')
->where('path', '.*')
->name('content.route');