353 lines
21 KiB
PHP
353 lines
21 KiB
PHP
@extends('layouts.nova')
|
|
|
|
@php
|
|
use App\Banner;
|
|
$src = $sourceArtwork;
|
|
@endphp
|
|
|
|
@push('head')
|
|
<title>{{ $page_title ?? 'Similar Artworks — Skinbase' }}</title>
|
|
<meta name="description" content="{{ $page_meta_description ?? '' }}">
|
|
<link rel="canonical" href="{{ $page_canonical ?? url()->current() }}">
|
|
<meta name="robots" content="{{ $page_robots ?? 'noindex,follow' }}">
|
|
|
|
{{-- OpenGraph --}}
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}" />
|
|
<meta property="og:title" content="{{ $page_title ?? 'Similar Artworks' }}" />
|
|
<meta property="og:description" content="{{ $page_meta_description ?? '' }}" />
|
|
@if(!empty($src->thumb_lg))
|
|
<meta property="og:image" content="{{ $src->thumb_lg }}" />
|
|
@endif
|
|
<meta property="og:site_name" content="Skinbase" />
|
|
|
|
{{-- Twitter card --}}
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content="{{ $page_title ?? 'Similar Artworks' }}" />
|
|
<meta name="twitter:description" content="{{ $page_meta_description ?? '' }}" />
|
|
@if(!empty($src->thumb_lg))
|
|
<meta name="twitter:image" content="{{ $src->thumb_lg }}" />
|
|
@endif
|
|
|
|
{{-- Breadcrumb structured data --}}
|
|
@if(isset($breadcrumbs) && $breadcrumbs->isNotEmpty())
|
|
<script type="application/ld+json">
|
|
{!! json_encode([
|
|
'@context' => 'https://schema.org',
|
|
'@type' => 'BreadcrumbList',
|
|
'itemListElement' => $breadcrumbs->values()->map(fn ($crumb, $i) => [
|
|
'@type' => 'ListItem',
|
|
'position' => $i + 1,
|
|
'name' => $crumb->name,
|
|
'item' => url($crumb->url),
|
|
])->all(),
|
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
|
|
</script>
|
|
@endif
|
|
@endpush
|
|
|
|
@section('content')
|
|
<div class="container-fluid legacy-page">
|
|
@php Banner::ShowResponsiveAd(); @endphp
|
|
|
|
$useUnifiedSeo = true;
|
|
<div class="pt-0">
|
|
|
|
{{-- Source info --}}
|
|
<div class="min-w-0 flex-1 space-y-2">
|
|
<h1 class="text-2xl font-bold leading-tight text-white md:text-3xl">
|
|
Artworks similar to
|
|
<a href="{{ $src->url }}" class="underline decoration-white/20 underline-offset-4 transition hover:decoration-sky-400 focus-visible:outline-none">{{ $src->title }}</a>
|
|
</h1>
|
|
|
|
{{-- Author & category --}}
|
|
<div class="flex flex-wrap items-center gap-3 text-sm text-white/50">
|
|
@if(!empty($src->author_name))
|
|
<span class="flex items-center gap-2">
|
|
@if(!empty($src->author_avatar))
|
|
<img src="{{ $src->author_avatar }}"
|
|
alt="{{ $src->author_name }}"
|
|
class="h-5 w-5 rounded-full object-cover ring-1 ring-white/20"
|
|
onerror="this.style.display='none'">
|
|
@endif
|
|
<a href="/{{ $src->author_username }}" class="font-medium text-white/70 hover:text-white transition">{{ $src->author_name }}</a>
|
|
</span>
|
|
@endif
|
|
|
|
@if(!empty($src->category_name))
|
|
<span class="inline-flex items-center gap-1 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-xs font-medium text-white/60">
|
|
{{ $src->category_name }}
|
|
</span>
|
|
@endif
|
|
|
|
@if(!empty($src->content_type_name))
|
|
<span class="inline-flex items-center gap-1 rounded-full border border-sky-400/20 bg-sky-400/[0.08] px-2.5 py-0.5 text-xs font-medium text-sky-300">
|
|
{{ $src->content_type_name }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Tags --}}
|
|
@if(!empty($src->tag_slugs))
|
|
<div class="flex flex-wrap gap-1.5">
|
|
@foreach($src->tag_slugs as $tagSlug)
|
|
<a href="{{ route('tags.show', $tagSlug) }}"
|
|
class="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-0.5 text-xs text-white/50 transition hover:border-sky-400/30 hover:bg-sky-400/[0.07] hover:text-white/80">
|
|
#{{ $tagSlug }}
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Actions --}}
|
|
<div class="flex items-center gap-3 pt-1">
|
|
<a href="{{ $src->url }}"
|
|
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60">
|
|
<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
Back to artwork
|
|
</a>
|
|
|
|
@if(!empty($src->content_type_slug))
|
|
<a href="/{{ $src->content_type_slug }}"
|
|
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.08] hover:text-white/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60">
|
|
Browse {{ $src->content_type_name ?: 'artworks' }}
|
|
</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ══════════════════════════════════════════════════════════════ --}}
|
|
{{-- RESULTS SECTION (loaded asynchronously) --}}
|
|
{{-- ══════════════════════════════════════════════════════════════ --}}
|
|
<section class="px-6 pb-10 pt-8 md:px-10" id="similar-results-section" data-artwork-id="{{ $src->id }}">
|
|
|
|
{{-- Section heading --}}
|
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<h2 class="text-lg font-semibold text-white">
|
|
<span class="mr-1">✨</span> Similar Artworks
|
|
<span id="similar-results-count" class="ml-2 text-sm font-normal text-white/40" style="display:none;"></span>
|
|
</h2>
|
|
<span id="similar-source-badge"></span>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Loading skeleton --}}
|
|
<div id="similar-loading-skeleton">
|
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5">
|
|
@for($i = 0; $i < 12; $i++)
|
|
<div class="animate-pulse">
|
|
<div class="aspect-[3/4] rounded-2xl bg-white/[0.06]"></div>
|
|
<div class="mt-2 flex items-center gap-2">
|
|
<div class="h-5 w-5 rounded-full bg-white/[0.06]"></div>
|
|
<div class="h-3 w-24 rounded bg-white/[0.06]"></div>
|
|
</div>
|
|
</div>
|
|
@endfor
|
|
</div>
|
|
<p class="mt-6 text-center text-sm text-white/30">
|
|
<svg class="mr-2 inline-block h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
|
</svg>
|
|
Finding similar artworks…
|
|
</p>
|
|
</div>
|
|
|
|
{{-- Error state (hidden by default) --}}
|
|
<div id="similar-error-state" style="display:none;">
|
|
<div class="flex flex-col items-center justify-center rounded-2xl border border-rose-400/20 bg-rose-400/[0.06] px-8 py-16 text-center">
|
|
<div class="mb-4 rounded-full border border-rose-300/20 bg-rose-500/10 p-5">
|
|
<svg class="h-8 w-8 text-rose-300/60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-base font-semibold text-white">Could not load similar artworks</h3>
|
|
<p class="mt-2 max-w-sm text-sm text-white/50">Something went wrong. Please try again.</p>
|
|
<button type="button" id="similar-retry-btn"
|
|
class="mt-5 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Empty state (hidden by default) --}}
|
|
<div id="similar-empty-state" style="display:none;">
|
|
<div class="flex flex-col items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.03] px-8 py-16 text-center">
|
|
<div class="mb-4 rounded-full border border-white/10 bg-white/[0.05] p-5">
|
|
<svg class="h-8 w-8 text-white/30" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-base font-semibold text-white">No similar artworks found yet</h3>
|
|
<p class="mt-2 max-w-sm text-sm text-white/50">
|
|
We could not find strong matches for this artwork right now. Try browsing the full gallery or check back later.
|
|
</p>
|
|
<div class="mt-6 flex flex-wrap justify-center gap-3">
|
|
<a href="{{ $sourceArtwork->url }}"
|
|
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">
|
|
← Back to artwork
|
|
</a>
|
|
<a href="/explore"
|
|
class="inline-flex items-center gap-2 rounded-xl border border-sky-400/20 bg-sky-400/[0.08] px-4 py-2.5 text-sm font-medium text-sky-300 transition hover:bg-sky-400/[0.14] hover:text-sky-200">
|
|
Browse Explore
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Masonry grid mount point (hidden until loaded) --}}
|
|
<div id="similar-masonry-mount" style="display:none;"
|
|
data-gallery-type="similar"
|
|
data-limit="24"
|
|
class="min-h-32"
|
|
></div>
|
|
|
|
{{-- Pagination (hidden until loaded) --}}
|
|
<div id="similar-pagination" style="display:none;" class="mt-10 flex items-center justify-center gap-3"></div>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
@vite('resources/js/entry-masonry-gallery.jsx')
|
|
<script>
|
|
(function () {
|
|
const section = document.getElementById('similar-results-section');
|
|
if (!section) return;
|
|
|
|
const artworkId = section.dataset.artworkId;
|
|
const skeleton = document.getElementById('similar-loading-skeleton');
|
|
const errorState = document.getElementById('similar-error-state');
|
|
const emptyState = document.getElementById('similar-empty-state');
|
|
const masonryMount = document.getElementById('similar-masonry-mount');
|
|
const pagination = document.getElementById('similar-pagination');
|
|
const countEl = document.getElementById('similar-results-count');
|
|
const badgeEl = document.getElementById('similar-source-badge');
|
|
const retryBtn = document.getElementById('similar-retry-btn');
|
|
|
|
function showOnly(el) {
|
|
[skeleton, errorState, emptyState, masonryMount, pagination].forEach(function (node) {
|
|
if (node) node.style.display = 'none';
|
|
});
|
|
if (el) el.style.display = '';
|
|
}
|
|
|
|
function renderSourceBadge(source) {
|
|
if (!badgeEl) return;
|
|
var badges = {
|
|
visual: { classes: 'border-violet-400/25 bg-violet-400/[0.08] text-violet-300', dot: 'bg-violet-400', text: 'Visual similarity' },
|
|
hybrid: { classes: 'border-emerald-400/20 bg-emerald-400/[0.08] text-emerald-300', dot: 'bg-emerald-400', text: 'AI similarity' },
|
|
tags: { classes: 'border-white/[0.08] bg-white/[0.04] text-white/40', dot: '', text: 'Tag match' },
|
|
};
|
|
var b = badges[source] || badges.tags;
|
|
badgeEl.innerHTML = '<span class="inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] ' + b.classes + '">'
|
|
+ (b.dot ? '<span class="h-1.5 w-1.5 rounded-full ' + b.dot + '"></span>' : '')
|
|
+ b.text + '</span>';
|
|
}
|
|
|
|
function renderPagination(data) {
|
|
if (!pagination) return;
|
|
if (data.last_page <= 1) return;
|
|
|
|
var html = '';
|
|
|
|
if (data.current_page <= 1) {
|
|
html += '<span class="inline-flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.03] px-4 py-2.5 text-sm text-white/30 cursor-default select-none">'
|
|
+ '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>'
|
|
+ 'Previous</span>';
|
|
} else {
|
|
html += '<a href="' + data.prev_page_url + '" class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">'
|
|
+ '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>'
|
|
+ 'Previous</a>';
|
|
}
|
|
|
|
html += '<span class="text-sm text-white/40">Page ' + data.current_page + ' of ' + data.last_page + '</span>';
|
|
|
|
if (data.current_page >= data.last_page) {
|
|
html += '<span class="inline-flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.03] px-4 py-2.5 text-sm text-white/30 cursor-default select-none">'
|
|
+ 'Next<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg></span>';
|
|
} else {
|
|
html += '<a href="' + data.next_page_url + '" class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-medium text-white/80 transition hover:bg-white/[0.10] hover:text-white">'
|
|
+ 'Next<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg></a>';
|
|
}
|
|
|
|
pagination.innerHTML = html;
|
|
pagination.style.display = '';
|
|
}
|
|
|
|
function loadResults(page) {
|
|
showOnly(skeleton);
|
|
if (pagination) pagination.style.display = 'none';
|
|
|
|
var url = '/art/' + encodeURIComponent(artworkId) + '/similar-results';
|
|
if (page && page > 1) url += '?page=' + page;
|
|
|
|
fetch(url, { headers: { Accept: 'application/json' }, credentials: 'same-origin' })
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
return res.json();
|
|
})
|
|
.then(function (json) {
|
|
if (!json.data || json.data.length === 0) {
|
|
showOnly(emptyState);
|
|
return;
|
|
}
|
|
|
|
renderSourceBadge(json.similarity_source || 'tags');
|
|
|
|
if (countEl) {
|
|
countEl.textContent = '(' + Number(json.total).toLocaleString() + ')';
|
|
countEl.style.display = '';
|
|
}
|
|
|
|
// Hydrate the masonry gallery mount
|
|
masonryMount.setAttribute('data-react-masonry-gallery', '');
|
|
masonryMount.setAttribute('data-artworks', JSON.stringify(json.data));
|
|
if (json.next_page_url) {
|
|
masonryMount.setAttribute('data-next-page-url', json.next_page_url);
|
|
} else {
|
|
masonryMount.removeAttribute('data-next-page-url');
|
|
}
|
|
showOnly(masonryMount);
|
|
|
|
// Re-trigger React masonry hydration
|
|
if (window.__hydrateMasonryGalleries) {
|
|
window.__hydrateMasonryGalleries();
|
|
} else {
|
|
// Dispatch a custom event as fallback
|
|
window.dispatchEvent(new CustomEvent('masonry-gallery:hydrate'));
|
|
}
|
|
|
|
renderPagination(json);
|
|
})
|
|
.catch(function () {
|
|
showOnly(errorState);
|
|
});
|
|
}
|
|
|
|
if (retryBtn) {
|
|
retryBtn.addEventListener('click', function () {
|
|
loadResults(1);
|
|
});
|
|
}
|
|
|
|
// Parse page from URL if present
|
|
var urlParams = new URLSearchParams(window.location.search);
|
|
var initialPage = parseInt(urlParams.get('page'), 10) || 1;
|
|
loadResults(initialPage);
|
|
})();
|
|
</script>
|
|
@endpush
|