gallery fix

This commit is contained in:
2026-02-21 21:39:23 +01:00
parent e4e0bdf8f1
commit 48e2055b6a
20 changed files with 1064 additions and 481 deletions

View File

@@ -0,0 +1,183 @@
@props([
'art',
'loading' => 'lazy',
'fetchpriority' => null,
])
@php
if (isset($art) && (is_array($art) || $art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection)) {
$first = null;
if (is_array($art)) {
$first = reset($art);
} elseif ($art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection) {
$first = $art->first();
}
if ($first) {
$art = $first;
}
}
$title = trim((string) ($art->name ?? $art->title ?? 'Untitled artwork'));
$author = trim((string) (
$art->uname
?? $art->author_name
?? $art->author
?? ($art->user->name ?? null)
?? ($art->user->username ?? null)
?? 'Skinbase'
));
$username = trim((string) (
$art->username
?? ($art->user->username ?? null)
?? ''
));
$category = trim((string) ($art->category_name ?? $art->category ?? ''));
$avatarUserId = $art->user->id ?? $art->user_id ?? null;
$avatarHash = $art->user->profile->avatar_hash ?? $art->avatar_hash ?? null;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($avatarUserId ?? 0), $avatarHash, 40);
$license = trim((string) ($art->license ?? 'Standard'));
$resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : '')));
$safeInt = function ($value, $fallback = 0) {
if (is_numeric($value)) {
return (int) $value;
}
if (is_array($value)) {
return count($value);
}
if (is_object($value)) {
if (method_exists($value, 'count')) {
return (int) $value->count();
}
if ($value instanceof Countable) {
return (int) count($value);
}
}
return (int) $fallback;
};
$likes = $safeInt($art->likes ?? $art->favourites ?? 0);
$comments = $safeInt($art->comments_count ?? $art->comment_count ?? $art->comments ?? 0);
$imgSrc = (string) ($art->thumb ?? $art->thumbnail_url ?? '/images/placeholder.jpg');
$imgSrcset = (string) ($art->thumb_srcset ?? $art->thumbnail_srcset ?? $imgSrc);
$imgAvifSrcset = (string) ($art->thumb_avif_srcset ?? $imgSrcset);
$imgWebpSrcset = (string) ($art->thumb_webp_srcset ?? $imgSrcset);
$resolveDimension = function ($value, string $field, $fallback) {
if (is_numeric($value)) {
return (int) $value;
}
if (is_array($value)) {
$current = reset($value);
return is_numeric($current) ? (int) $current : (int) $fallback;
}
if (is_object($value)) {
if (method_exists($value, 'first')) {
$first = $value->first();
if (is_object($first) && isset($first->{$field})) {
return (int) ($first->{$field} ?: $fallback);
}
}
if (isset($value->{$field})) {
return (int) $value->{$field};
}
}
return (int) $fallback;
};
$imgWidth = max(1, $resolveDimension($art->width ?? null, 'width', 800));
$imgHeight = max(1, $resolveDimension($art->height ?? null, 'height', 600));
$imgAspectRatio = $imgWidth . ' / ' . $imgHeight;
$contentUrl = $imgSrc;
$cardUrl = (string) ($art->url ?? '');
if ($cardUrl === '' || $cardUrl === '#') {
if (isset($art->id) && is_numeric($art->id)) {
$cardUrl = '/art/' . (int) $art->id . '/' . \Illuminate\Support\Str::slug($title);
} else {
$cardUrl = '#';
}
}
$authorUrl = $username !== '' ? '/@' . strtolower($username) : null;
$metaParts = [];
if ($resolution !== '') {
$metaParts[] = $resolution;
}
if ($category !== '') {
$metaParts[] = $category;
}
if ($license !== '') {
$metaParts[] = $license;
}
@endphp
<article class="nova-card gallery-item artwork" itemscope itemtype="https://schema.org/ImageObject">
<meta itemprop="name" content="{{ $title }}">
<meta itemprop="contentUrl" content="{{ $contentUrl }}">
<meta itemprop="creator" content="{{ $author }}">
<meta itemprop="license" content="{{ $license }}">
<a href="{{ $cardUrl }}" itemprop="url" class="group relative block overflow-hidden rounded-2xl ring-1 ring-white/5 bg-black/20 shadow-lg shadow-black/40 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70">
@if($category !== '')
<div class="absolute left-3 top-3 z-30 rounded-md bg-black/55 px-2 py-1 text-xs text-white backdrop-blur-sm">{{ $category }}</div>
@endif
<div class="nova-card-media relative overflow-hidden bg-neutral-900" style="aspect-ratio: {{ $imgAspectRatio }};">
<div class="absolute inset-0 bg-gradient-to-br from-white/10 via-white/5 to-transparent"></div>
<picture>
<source srcset="{{ $imgAvifSrcset }}" type="image/avif">
<source srcset="{{ $imgWebpSrcset }}" type="image/webp">
<img
src="{{ $imgSrc }}"
srcset="{{ $imgSrcset }}"
sizes="(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 20vw"
loading="{{ $loading }}"
decoding="{{ $loading === 'eager' ? 'sync' : 'async' }}"
@if($fetchpriority) fetchpriority="{{ $fetchpriority }}" @endif
@if($loading !== 'eager') data-blur-preview @endif
alt="{{ e($title) }}"
width="{{ $imgWidth }}"
height="{{ $imgHeight }}"
class="h-full w-full object-cover transition-[transform,filter] duration-300 ease-out group-hover:scale-[1.04]"
itemprop="thumbnailUrl"
/>
</picture>
<div class="absolute right-3 top-3 z-30 flex items-center gap-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-visible:opacity-100">
<span class="inline-flex items-center rounded-md bg-black/60 px-2 py-1 text-[11px] font-medium text-white ring-1 ring-white/10">View</span>
@if($authorUrl)
<span class="inline-flex items-center rounded-md bg-black/60 px-2 py-1 text-[11px] font-medium text-white ring-1 ring-white/10">Profile</span>
@endif
</div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100">
<div class="truncate text-sm font-semibold text-white">{{ $title }}</div>
<div class="mt-1 flex items-center justify-between gap-3 text-xs text-white/80">
<span class="truncate flex items-center gap-2">
<img src="{{ $avatarUrl }}" alt="Avatar of {{ e($author) }}" class="w-6 h-6 rounded-full object-cover">
<span class="truncate">
<span>{{ $author }}</span>
@if($username !== '')
<span class="text-white/60">{{ '@' . $username }}</span>
@endif
</span>
</span>
<span class="shrink-0"> {{ $likes }} · 💬 {{ $comments }}</span>
</div>
@if(!empty($metaParts))
<div class="mt-1 text-[11px] text-white/70">{{ implode(' • ', $metaParts) }}</div>
@endif
</div>
</div>
<span class="sr-only">{{ $title }} by {{ $author }}</span>
</a>
</article>

View File

@@ -0,0 +1,8 @@
<div class="nova-skeleton-card" aria-hidden="true">
<div class="nova-skeleton-media"></div>
<div class="nova-skeleton-body">
<div class="nova-skeleton-line w-3/4"></div>
<div class="nova-skeleton-line w-1/2"></div>
<div class="nova-skeleton-pill w-20"></div>
</div>
</div>

View File

@@ -1,5 +1,7 @@
@extends('layouts.nova')
@php($gridV2 = request()->query('grid') === 'v2')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Favourites</h1>
@@ -20,44 +22,28 @@
@if($artworks->isEmpty())
<p class="text-sm text-gray-500">You have no favourites yet.</p>
@else
<div class="overflow-x-auto bg-panel rounded">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-panel">
<th class="p-2 text-left">Thumb</th>
<th class="p-2 text-left">Name</th>
<th class="p-2 text-left">Author</th>
<th class="p-2 text-left">Published</th>
<th class="p-2 text-left">Actions</th>
</tr>
</thead>
<tbody>
@foreach($artworks as $art)
<tr class="border-b border-panel">
<td class="p-2 w-24">
<a href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}">
<img src="{{ $art->thumb ?? '/gfx/sb_join.jpg' }}" alt="{{ $art->title }}" class="w-20 h-12 object-cover rounded" />
</a>
</td>
<td class="p-2">
<a href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}" class="font-medium">{{ $art->title }}</a>
</td>
<td class="p-2">{{ $art->author }}</td>
<td class="p-2">{{ optional($art->published_at)->format('Y-m-d') }}</td>
<td class="p-2">
<form method="POST" action="{{ route('dashboard.favorites.destroy', ['artwork' => $art->id]) }}" onsubmit="return confirm('Really remove from favourites?');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-500 hover:underline">Remove</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<section data-nova-gallery data-gallery-type="dashboard-favorites">
<div class="{{ $gridV2 ? 'gallery' : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6' }}" data-gallery-grid>
@foreach($artworks as $art)
<div class="relative gallery-item">
<x-artwork-card :art="$art" />
<div class="absolute right-2 top-2 z-40">
<form method="POST" action="{{ route('dashboard.favorites.destroy', ['artwork' => $art->id]) }}" onsubmit="return confirm('Really remove from favourites?');">
@csrf
@method('DELETE')
<button type="submit" class="rounded-md border border-white/15 bg-black/60 px-2 py-1 text-xs text-red-300 hover:text-red-200">Remove</button>
</form>
</div>
</div>
@endforeach
</div>
<div class="mt-6">{{ $artworks->links() }}</div>
<div class="mt-6" data-gallery-pagination>{{ $artworks->links() }}</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-8" data-gallery-skeleton></div>
</section>
@endif
</div>
@endsection

View File

@@ -2,6 +2,7 @@
@php
use App\Banner;
$gridV2 = request()->query('grid') === 'v2';
@endphp
@section('content')
@@ -94,9 +95,13 @@
</div>
<section class="px-6 pb-10 pt-8 md:px-10" data-nova-gallery data-gallery-type="{{ $gallery_type ?? 'browse' }}">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 force-5" data-gallery-grid>
<div class="{{ $gridV2 ? 'gallery' : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 force-5' }}" data-gallery-grid>
@forelse ($artworks as $art)
@include('legacy._artwork_card', ['art' => $art])
<x-artwork-card
:art="$art"
:loading="$loop->index < 8 ? 'eager' : 'lazy'"
:fetchpriority="$loop->index === 0 ? 'high' : null"
/>
@empty
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
@@ -112,6 +117,9 @@
{{ method_exists($artworks, 'withQueryString') ? $artworks->withQueryString()->links() : $artworks->links() }}
@endif
</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-8" data-gallery-skeleton></div>
</section>
</main>
@@ -122,6 +130,7 @@
@endsection
@push('styles')
@if(! $gridV2)
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
@@ -206,6 +215,7 @@
background: linear-gradient(180deg, rgba(17,24,39,0) 0%, rgba(7,10,15,0.9) 60%, rgba(7,10,15,1) 100%);
}
</style>
@endif
@endpush
@push('scripts')

View File

@@ -1,5 +1,8 @@
@php
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
@endphp
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<html lang="{{ app()->getLocale() }}" data-grid-version="{{ $gridVersion }}">
<head>
<title>{{ $page_title ?? 'Skinbase' }}</title>
@@ -7,15 +10,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ $page_meta_description ?? '' }}">
<meta name="keywords" content="{{ $page_meta_keywords ?? '' }}">
@isset($page_robots)
<meta name="robots" content="{{ $page_robots }}" />
@endisset
@isset($page_canonical)
<link rel="canonical" href="{{ $page_canonical }}" />
@endisset
@isset($page_rel_prev)
<link rel="prev" href="{{ $page_rel_prev }}" />
@endisset
@isset($page_rel_next)
<link rel="next" href="{{ $page_rel_next }}" />
@endisset
<!-- Icons (kept for now to preserve current visual output) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<link rel="shortcut icon" href="/favicon.ico">
@vite(['resources/css/app.css','resources/scss/nova.scss','resources/js/nova.js'])
@vite(['resources/css/app.css','resources/css/nova-grid.css','resources/scss/nova.scss','resources/js/nova.js'])
<style>
/* Gallery loading overlay */
.nova-loader-overlay {

View File

@@ -1,5 +1,7 @@
@extends('layouts.nova')
@php($gridV2 = request()->query('grid') === 'v2')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
@@ -14,18 +16,15 @@
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>Newest Artworks</strong></div>
<div class="panel-body">
<div class="gallery-grid">
<div class="{{ $gridV2 ? 'gallery' : 'gallery-grid' }}" data-nova-gallery data-gallery-type="profile" data-gallery-grid>
@foreach($artworks as $art)
<div class="thumb-card effect2">
<a href="/art/{{ $art->id }}/{{ Str::slug($art->name ?? '') }}" class="thumb-link">
<img src="{{ $art->thumb }}" srcset="{{ $art->thumb_srcset }}" alt="{{ $art->name }}" class="img-responsive" loading="lazy" decoding="async">
</a>
<div class="thumb-meta">
<div class="thumb-title">{{ $art->name }}</div>
</div>
</div>
<x-artwork-card :art="$art" />
@endforeach
</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-8" data-gallery-skeleton></div>
</div>
</div>
</div>
@@ -43,3 +42,7 @@
</div>
</div>
@endsection
@push('scripts')
<script src="/js/legacy-gallery-init.js" defer></script>
@endpush

View File

@@ -1,9 +1,11 @@
@php($gridV2 = request()->query('grid') === 'v2')
{{-- Latest uploads grid use same Nova gallery layout as /browse --}}
<section class="px-6 pb-10 pt-6 md:px-10" data-nova-gallery data-gallery-type="home-uploads">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" data-gallery-grid>
<div class="{{ $gridV2 ? 'gallery' : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6' }}" data-gallery-grid>
@forelse($latestUploads as $upload)
@include('web.partials._artwork_card', ['art' => $upload])
<x-artwork-card :art="$upload" />
@empty
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>No uploads yet</strong></div>
@@ -15,10 +17,14 @@
<div class="flex justify-center mt-10" data-gallery-pagination>
{{-- no pagination for home grid; kept for parity with browse layout --}}
</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-8" data-gallery-skeleton></div>
</section>
@push('styles')
@if(! $gridV2)
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
@@ -39,6 +45,7 @@
[data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; }
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
</style>
@endif
@endpush
@push('scripts')

View File

@@ -1,128 +1 @@
@php
// If a Collection or array was passed accidentally, pick the first item.
if (isset($art) && (is_array($art) || $art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection)) {
$first = null;
if (is_array($art)) {
$first = reset($art);
} elseif ($art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection) {
$first = $art->first();
}
if ($first) {
$art = $first;
}
}
$title = trim((string) ($art->name ?? $art->title ?? 'Untitled artwork'));
$author = trim((string) (
$art->uname
?? $art->author_name
?? $art->author
?? ($art->user->name ?? null)
?? ($art->user->username ?? null)
?? 'Skinbase'
));
$category = trim((string) ($art->category_name ?? $art->category ?? ''));
$avatarUserId = $art->user->id ?? $art->user_id ?? null;
$avatarHash = $art->user->profile->avatar_hash ?? $art->avatar_hash ?? null;
$avatar_url = \App\Support\AvatarUrl::forUser((int) ($avatarUserId ?? 0), $avatarHash, 40);
$license = trim((string) ($art->license ?? 'Standard'));
$resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : '')));
// 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)) {
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);
$comments = $safeInt($art->comments_count ?? $art->comment_count ?? $art->comments ?? 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);
$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);
}
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 ?? '#');
@endphp
<article class="nova-card artwork" itemscope itemtype="https://schema.org/ImageObject">
<meta itemprop="name" content="{{ $title }}">
<meta itemprop="contentUrl" content="{{ $contentUrl }}">
<meta itemprop="creator" content="{{ $author }}">
<meta itemprop="license" content="{{ $license }}">
<a href="{{ $cardUrl }}" itemprop="url" class="group relative block overflow-hidden rounded-2xl ring-1 ring-white/5 bg-black/20 shadow-lg shadow-black/40 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70">
@if(!empty($category))
<div class="absolute left-3 top-3 z-30 rounded-md bg-black/55 px-2 py-1 text-xs text-white backdrop-blur-sm">{{ $category }}</div>
@endif
<div class="nova-card-media relative aspect-[16/10] overflow-hidden bg-neutral-900">
<picture>
<source srcset="{{ $img_avif_srcset }}" type="image/avif">
<source srcset="{{ $img_webp_srcset }}" type="image/webp">
<img
src="{{ $img_src }}"
srcset="{{ $img_srcset }}"
sizes="(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 20vw"
loading="lazy"
decoding="async"
alt="{{ e($title) }}"
width="{{ $img_width }}"
height="{{ $img_height }}"
class="h-full w-full object-cover transition-transform duration-200 ease-out group-hover:scale-[1.04]"
itemprop="thumbnailUrl"
/>
</picture>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100">
<div class="truncate text-sm font-semibold text-white">{{ $title }}</div>
<div class="mt-1 flex items-center justify-between gap-3 text-xs text-white/80">
<span class="truncate flex items-center gap-2">
<img src="{{ $avatar_url }}" alt="Avatar of {{ e($author) }}" class="w-6 h-6 rounded-full object-cover">
<span class="truncate">by {{ $author }}</span>
</span>
<span class="shrink-0"> {{ $likes }} · 💬 {{ $comments }}</span>
</div>
<div class="mt-1 text-[11px] text-white/70">
@php
$meta_parts = [];
if (!empty($resolution)) $meta_parts[] = $resolution;
if (!empty($category)) $meta_parts[] = $category;
if (!empty($license)) $meta_parts[] = $license;
@endphp
{{ implode(' • ', $meta_parts) }}
</div>
</div>
</div>
<span class="sr-only">{{ $title }} by {{ $author }}</span>
</a>
</article>
<x-artwork-card :art="$art" />