gallery
This commit is contained in:
@@ -1,60 +1,280 @@
|
||||
// Modern gallery init: wait for images to decode, then init Isotope/Masonry
|
||||
// Nova gallery progressive enhancement:
|
||||
// - Masonry-like responsive layout (CSS grid row spans)
|
||||
// - Infinite scroll on top of server pagination (SEO-safe fallback)
|
||||
// - Skeleton placeholders while loading
|
||||
// - Virtualized rendering hints for long feeds
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var MAX_DOM_CARDS_FOR_VIRTUAL_HINT = 220;
|
||||
var LOAD_TRIGGER_MARGIN = '900px';
|
||||
|
||||
function toArray(list) {
|
||||
return Array.prototype.slice.call(list || []);
|
||||
}
|
||||
|
||||
function queryNextPageUrl(root) {
|
||||
var pagination = root.querySelector('[data-gallery-pagination]');
|
||||
if (!pagination) return null;
|
||||
var next = pagination.querySelector('a[rel="next"], a[aria-label="Next »"], a[aria-label="Next"], a[aria-label="pagination.next"]');
|
||||
return next ? next.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
function setSkeleton(root, active, count) {
|
||||
var box = root.querySelector('[data-gallery-skeleton]');
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
if (!active) {
|
||||
box.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
box.classList.add('is-loading');
|
||||
var total = Math.max(4, count || 8);
|
||||
for (var i = 0; i < total; i += 1) {
|
||||
var sk = document.createElement('div');
|
||||
sk.className = 'nova-skeleton-card';
|
||||
box.appendChild(sk);
|
||||
}
|
||||
}
|
||||
|
||||
function waitForImages(container) {
|
||||
var imgs = Array.prototype.slice.call(container.querySelectorAll('img'));
|
||||
var imgs = toArray(container.querySelectorAll('img'));
|
||||
var promises = imgs.map(function (img) {
|
||||
try {
|
||||
if (!img.complete) {
|
||||
return img.decode().catch(function(){ /* ignore */ });
|
||||
}
|
||||
// already complete: still attempt decode if supported
|
||||
return img.decode ? img.decode().catch(function(){}) : Promise.resolve();
|
||||
if (img.decode) return img.decode().catch(function () { return null; });
|
||||
} catch (e) {
|
||||
return Promise.resolve();
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
async function initGallery() {
|
||||
var grid = document.querySelector('.gallery-grid');
|
||||
function applyMasonry(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
var rowSize = 8;
|
||||
var gap = 16;
|
||||
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
cards.forEach(function (card) {
|
||||
var media = card.querySelector('.nova-card-media') || card;
|
||||
var height = media.getBoundingClientRect().height || 200;
|
||||
// Lower the minimum forced span to avoid large empty space at the bottom.
|
||||
// Previously this used a very large minimum (18) which reserved too many rows.
|
||||
var minSpan = 1;
|
||||
var span = Math.max(minSpan, Math.ceil((height + gap) / (rowSize + gap)));
|
||||
card.style.gridRowEnd = 'span ' + span;
|
||||
});
|
||||
}
|
||||
|
||||
function applyVirtualizationHints(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
if (cards.length <= MAX_DOM_CARDS_FOR_VIRTUAL_HINT) {
|
||||
cards.forEach(function (card) {
|
||||
card.style.contentVisibility = '';
|
||||
card.style.containIntrinsicSize = '';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var viewportTop = window.scrollY;
|
||||
var viewportBottom = viewportTop + window.innerHeight;
|
||||
|
||||
cards.forEach(function (card) {
|
||||
var rect = card.getBoundingClientRect();
|
||||
var top = rect.top + viewportTop;
|
||||
var bottom = rect.bottom + viewportTop;
|
||||
var farAbove = bottom < viewportTop - 1400;
|
||||
var farBelow = top > viewportBottom + 2600;
|
||||
|
||||
if (farAbove || farBelow) {
|
||||
var h = Math.max(160, rect.height || 220);
|
||||
card.style.contentVisibility = 'auto';
|
||||
card.style.containIntrinsicSize = Math.round(h) + 'px';
|
||||
} else {
|
||||
card.style.contentVisibility = '';
|
||||
card.style.containIntrinsicSize = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function extractAndAppendCards(root, html) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(html, 'text/html');
|
||||
var incomingGrid = doc.querySelector('[data-gallery-grid]');
|
||||
if (!incomingGrid) return { appended: 0, nextUrl: null };
|
||||
|
||||
var targetGrid = root.querySelector('[data-gallery-grid]');
|
||||
if (!targetGrid) return { appended: 0, nextUrl: null };
|
||||
|
||||
var cards = toArray(incomingGrid.querySelectorAll('.nova-card'));
|
||||
cards.forEach(function (card) {
|
||||
targetGrid.appendChild(card);
|
||||
});
|
||||
|
||||
var incomingPagination = doc.querySelector('[data-gallery-pagination]');
|
||||
var currentPagination = root.querySelector('[data-gallery-pagination]');
|
||||
if (incomingPagination && currentPagination) {
|
||||
currentPagination.innerHTML = incomingPagination.innerHTML;
|
||||
}
|
||||
|
||||
return {
|
||||
appended: cards.length,
|
||||
nextUrl: queryNextPageUrl(root)
|
||||
};
|
||||
}
|
||||
|
||||
function initOne(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
|
||||
// loader overlay element (created lazily)
|
||||
var loader = null;
|
||||
function ensureLoader() {
|
||||
if (loader) return loader;
|
||||
loader = document.createElement('div');
|
||||
loader.className = 'nova-loader-overlay';
|
||||
var inner = document.createElement('div');
|
||||
inner.className = 'nova-loader-spinner';
|
||||
loader.appendChild(inner);
|
||||
// place loader as child of root so it overlays grid area
|
||||
loader.style.display = 'none';
|
||||
root.style.position = root.style.position || '';
|
||||
root.appendChild(loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
function showLoader() { var l = ensureLoader(); l.style.display = 'flex'; }
|
||||
function hideLoader() { if (loader) loader.style.display = 'none'; }
|
||||
|
||||
root.classList.add('is-enhanced');
|
||||
|
||||
var state = {
|
||||
loading: false,
|
||||
nextUrl: queryNextPageUrl(root),
|
||||
done: false
|
||||
};
|
||||
|
||||
function relayout() {
|
||||
waitForImages(grid).then(function () {
|
||||
applyMasonry(root);
|
||||
applyVirtualizationHints(root);
|
||||
});
|
||||
}
|
||||
|
||||
var rafId = null;
|
||||
function onScrollOrResize() {
|
||||
if (rafId) return;
|
||||
rafId = window.requestAnimationFrame(function () {
|
||||
rafId = null;
|
||||
applyVirtualizationHints(root);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNextPage() {
|
||||
if (state.loading || state.done || !state.nextUrl) return;
|
||||
state.loading = true;
|
||||
|
||||
showLoader();
|
||||
|
||||
var sampleCards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
var skeletonCount = Math.min(12, Math.max(4, sampleCards.length ? sampleCards.slice(-4).length * 2 : 8));
|
||||
setSkeleton(root, true, skeletonCount);
|
||||
|
||||
try {
|
||||
await waitForImages(grid);
|
||||
var response = await window.fetch(state.nextUrl, {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load page');
|
||||
var html = await response.text();
|
||||
|
||||
var result = extractAndAppendCards(root, html);
|
||||
state.nextUrl = result.nextUrl;
|
||||
if (!state.nextUrl || result.appended === 0) {
|
||||
state.done = true;
|
||||
}
|
||||
|
||||
// Animate appended cards
|
||||
var appendedCards = toArray(grid.querySelectorAll('.nova-card')).slice(-result.appended);
|
||||
appendedCards.forEach(function (c) {
|
||||
c.classList.add('nova-card-enter');
|
||||
// trigger reflow then add active class
|
||||
requestAnimationFrame(function () { c.classList.add('nova-card-enter-active'); });
|
||||
c.addEventListener('transitionend', function te() { c.classList.remove('nova-card-enter', 'nova-card-enter-active'); c.removeEventListener('transitionend', te); });
|
||||
});
|
||||
|
||||
relayout();
|
||||
// After new cards appended, move the trigger to remain one-row-before-last.
|
||||
placeTrigger();
|
||||
} catch (e) {
|
||||
// continue even on failures
|
||||
}
|
||||
|
||||
// Prefer Isotope (legacy code included it). Fall back to Masonry.
|
||||
if (window.Isotope) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var iso = new Isotope(grid, {
|
||||
itemSelector: '.photo_frame',
|
||||
layoutMode: 'masonry',
|
||||
masonry: { columnWidth: '.photo_frame', gutter: 12 },
|
||||
percentPosition: true,
|
||||
fitWidth: true
|
||||
});
|
||||
} else if (window.Masonry) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var m = new Masonry(grid, {
|
||||
itemSelector: '.photo_frame',
|
||||
columnWidth: '.photo_frame',
|
||||
percentPosition: true,
|
||||
gutter: 12,
|
||||
fitWidth: true
|
||||
});
|
||||
state.done = true;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
setSkeleton(root, false);
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initGallery);
|
||||
var trigger = document.createElement('div');
|
||||
trigger.setAttribute('aria-hidden', 'true');
|
||||
trigger.className = 'h-px w-full';
|
||||
|
||||
function placeTrigger() {
|
||||
// Place the trigger inside the grid one row before the last row so
|
||||
// loading starts earlier (when the user reaches the penultimate row).
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
var colCount = 1;
|
||||
try {
|
||||
var cols = window.getComputedStyle(grid).getPropertyValue('grid-template-columns');
|
||||
if (cols) colCount = Math.max(1, cols.trim().split(/\s+/).length);
|
||||
} catch (e) {
|
||||
colCount = 1;
|
||||
}
|
||||
|
||||
var refIndex = Math.max(0, cards.length - colCount);
|
||||
var ref = cards[refIndex] || null;
|
||||
|
||||
if (trigger.parentNode) trigger.parentNode.removeChild(trigger);
|
||||
if (ref && ref.parentNode) {
|
||||
ref.parentNode.insertBefore(trigger, ref);
|
||||
} else {
|
||||
initGallery();
|
||||
grid.appendChild(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
if ('IntersectionObserver' in window) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) loadNextPage();
|
||||
});
|
||||
}, { root: null, rootMargin: LOAD_TRIGGER_MARGIN, threshold: 0 });
|
||||
// Place and observe the trigger. It will be moved after new cards are appended.
|
||||
placeTrigger();
|
||||
io.observe(trigger);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
relayout();
|
||||
onScrollOrResize();
|
||||
placeTrigger();
|
||||
}, { passive: true });
|
||||
window.addEventListener('scroll', onScrollOrResize, { passive: true });
|
||||
|
||||
relayout();
|
||||
placeTrigger();
|
||||
}
|
||||
|
||||
function init() {
|
||||
toArray(document.querySelectorAll('[data-nova-gallery]')).forEach(initOne);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,60 +1,280 @@
|
||||
// Modern gallery init: wait for images to decode, then init Isotope/Masonry
|
||||
// Nova gallery progressive enhancement:
|
||||
// - Masonry-like responsive layout (CSS grid row spans)
|
||||
// - Infinite scroll on top of server pagination (SEO-safe fallback)
|
||||
// - Skeleton placeholders while loading
|
||||
// - Virtualized rendering hints for long feeds
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var MAX_DOM_CARDS_FOR_VIRTUAL_HINT = 220;
|
||||
var LOAD_TRIGGER_MARGIN = '900px';
|
||||
|
||||
function toArray(list) {
|
||||
return Array.prototype.slice.call(list || []);
|
||||
}
|
||||
|
||||
function queryNextPageUrl(root) {
|
||||
var pagination = root.querySelector('[data-gallery-pagination]');
|
||||
if (!pagination) return null;
|
||||
var next = pagination.querySelector('a[rel="next"], a[aria-label="Next »"], a[aria-label="Next"], a[aria-label="pagination.next"]');
|
||||
return next ? next.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
function setSkeleton(root, active, count) {
|
||||
var box = root.querySelector('[data-gallery-skeleton]');
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
if (!active) {
|
||||
box.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
box.classList.add('is-loading');
|
||||
var total = Math.max(4, count || 8);
|
||||
for (var i = 0; i < total; i += 1) {
|
||||
var sk = document.createElement('div');
|
||||
sk.className = 'nova-skeleton-card';
|
||||
box.appendChild(sk);
|
||||
}
|
||||
}
|
||||
|
||||
function waitForImages(container) {
|
||||
var imgs = Array.prototype.slice.call(container.querySelectorAll('img'));
|
||||
var imgs = toArray(container.querySelectorAll('img'));
|
||||
var promises = imgs.map(function (img) {
|
||||
try {
|
||||
if (!img.complete) {
|
||||
return img.decode().catch(function(){ /* ignore */ });
|
||||
}
|
||||
// already complete: still attempt decode if supported
|
||||
return img.decode ? img.decode().catch(function(){}) : Promise.resolve();
|
||||
if (img.decode) return img.decode().catch(function () { return null; });
|
||||
} catch (e) {
|
||||
return Promise.resolve();
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
async function initGallery() {
|
||||
var grid = document.querySelector('.gallery-grid');
|
||||
function applyMasonry(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
var rowSize = 8;
|
||||
var gap = 16;
|
||||
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
cards.forEach(function (card) {
|
||||
var media = card.querySelector('.nova-card-media') || card;
|
||||
var height = media.getBoundingClientRect().height || 200;
|
||||
// Lower the minimum forced span to avoid large empty space at the bottom.
|
||||
// Previously this used a very large minimum (18) which reserved too many rows.
|
||||
var minSpan = 1;
|
||||
var span = Math.max(minSpan, Math.ceil((height + gap) / (rowSize + gap)));
|
||||
card.style.gridRowEnd = 'span ' + span;
|
||||
});
|
||||
}
|
||||
|
||||
function applyVirtualizationHints(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
if (cards.length <= MAX_DOM_CARDS_FOR_VIRTUAL_HINT) {
|
||||
cards.forEach(function (card) {
|
||||
card.style.contentVisibility = '';
|
||||
card.style.containIntrinsicSize = '';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var viewportTop = window.scrollY;
|
||||
var viewportBottom = viewportTop + window.innerHeight;
|
||||
|
||||
cards.forEach(function (card) {
|
||||
var rect = card.getBoundingClientRect();
|
||||
var top = rect.top + viewportTop;
|
||||
var bottom = rect.bottom + viewportTop;
|
||||
var farAbove = bottom < viewportTop - 1400;
|
||||
var farBelow = top > viewportBottom + 2600;
|
||||
|
||||
if (farAbove || farBelow) {
|
||||
var h = Math.max(160, rect.height || 220);
|
||||
card.style.contentVisibility = 'auto';
|
||||
card.style.containIntrinsicSize = Math.round(h) + 'px';
|
||||
} else {
|
||||
card.style.contentVisibility = '';
|
||||
card.style.containIntrinsicSize = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function extractAndAppendCards(root, html) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(html, 'text/html');
|
||||
var incomingGrid = doc.querySelector('[data-gallery-grid]');
|
||||
if (!incomingGrid) return { appended: 0, nextUrl: null };
|
||||
|
||||
var targetGrid = root.querySelector('[data-gallery-grid]');
|
||||
if (!targetGrid) return { appended: 0, nextUrl: null };
|
||||
|
||||
var cards = toArray(incomingGrid.querySelectorAll('.nova-card'));
|
||||
cards.forEach(function (card) {
|
||||
targetGrid.appendChild(card);
|
||||
});
|
||||
|
||||
var incomingPagination = doc.querySelector('[data-gallery-pagination]');
|
||||
var currentPagination = root.querySelector('[data-gallery-pagination]');
|
||||
if (incomingPagination && currentPagination) {
|
||||
currentPagination.innerHTML = incomingPagination.innerHTML;
|
||||
}
|
||||
|
||||
return {
|
||||
appended: cards.length,
|
||||
nextUrl: queryNextPageUrl(root)
|
||||
};
|
||||
}
|
||||
|
||||
function initOne(root) {
|
||||
var grid = root.querySelector('[data-gallery-grid]');
|
||||
if (!grid) return;
|
||||
|
||||
// loader overlay element (created lazily)
|
||||
var loader = null;
|
||||
function ensureLoader() {
|
||||
if (loader) return loader;
|
||||
loader = document.createElement('div');
|
||||
loader.className = 'nova-loader-overlay';
|
||||
var inner = document.createElement('div');
|
||||
inner.className = 'nova-loader-spinner';
|
||||
loader.appendChild(inner);
|
||||
// place loader as child of root so it overlays grid area
|
||||
loader.style.display = 'none';
|
||||
root.style.position = root.style.position || '';
|
||||
root.appendChild(loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
function showLoader() { var l = ensureLoader(); l.style.display = 'flex'; }
|
||||
function hideLoader() { if (loader) loader.style.display = 'none'; }
|
||||
|
||||
root.classList.add('is-enhanced');
|
||||
|
||||
var state = {
|
||||
loading: false,
|
||||
nextUrl: queryNextPageUrl(root),
|
||||
done: false
|
||||
};
|
||||
|
||||
function relayout() {
|
||||
waitForImages(grid).then(function () {
|
||||
applyMasonry(root);
|
||||
applyVirtualizationHints(root);
|
||||
});
|
||||
}
|
||||
|
||||
var rafId = null;
|
||||
function onScrollOrResize() {
|
||||
if (rafId) return;
|
||||
rafId = window.requestAnimationFrame(function () {
|
||||
rafId = null;
|
||||
applyVirtualizationHints(root);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNextPage() {
|
||||
if (state.loading || state.done || !state.nextUrl) return;
|
||||
state.loading = true;
|
||||
|
||||
showLoader();
|
||||
|
||||
var sampleCards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
var skeletonCount = Math.min(12, Math.max(4, sampleCards.length ? sampleCards.slice(-4).length * 2 : 8));
|
||||
setSkeleton(root, true, skeletonCount);
|
||||
|
||||
try {
|
||||
await waitForImages(grid);
|
||||
var response = await window.fetch(state.nextUrl, {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to load page');
|
||||
var html = await response.text();
|
||||
|
||||
var result = extractAndAppendCards(root, html);
|
||||
state.nextUrl = result.nextUrl;
|
||||
if (!state.nextUrl || result.appended === 0) {
|
||||
state.done = true;
|
||||
}
|
||||
|
||||
// Animate appended cards
|
||||
var appendedCards = toArray(grid.querySelectorAll('.nova-card')).slice(-result.appended);
|
||||
appendedCards.forEach(function (c) {
|
||||
c.classList.add('nova-card-enter');
|
||||
// trigger reflow then add active class
|
||||
requestAnimationFrame(function () { c.classList.add('nova-card-enter-active'); });
|
||||
c.addEventListener('transitionend', function te() { c.classList.remove('nova-card-enter', 'nova-card-enter-active'); c.removeEventListener('transitionend', te); });
|
||||
});
|
||||
|
||||
relayout();
|
||||
// After new cards appended, move the trigger to remain one-row-before-last.
|
||||
placeTrigger();
|
||||
} catch (e) {
|
||||
// continue even on failures
|
||||
}
|
||||
|
||||
// Prefer Isotope (legacy code included it). Fall back to Masonry.
|
||||
if (window.Isotope) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var iso = new Isotope(grid, {
|
||||
itemSelector: '.photo_frame',
|
||||
layoutMode: 'masonry',
|
||||
masonry: { columnWidth: '.photo_frame', gutter: 12 },
|
||||
percentPosition: true,
|
||||
fitWidth: true
|
||||
});
|
||||
} else if (window.Masonry) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var m = new Masonry(grid, {
|
||||
itemSelector: '.photo_frame',
|
||||
columnWidth: '.photo_frame',
|
||||
percentPosition: true,
|
||||
gutter: 12,
|
||||
fitWidth: true
|
||||
});
|
||||
state.done = true;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
setSkeleton(root, false);
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initGallery);
|
||||
var trigger = document.createElement('div');
|
||||
trigger.setAttribute('aria-hidden', 'true');
|
||||
trigger.className = 'h-px w-full';
|
||||
|
||||
function placeTrigger() {
|
||||
// Place the trigger inside the grid one row before the last row so
|
||||
// loading starts earlier (when the user reaches the penultimate row).
|
||||
var cards = toArray(grid.querySelectorAll('.nova-card'));
|
||||
var colCount = 1;
|
||||
try {
|
||||
var cols = window.getComputedStyle(grid).getPropertyValue('grid-template-columns');
|
||||
if (cols) colCount = Math.max(1, cols.trim().split(/\s+/).length);
|
||||
} catch (e) {
|
||||
colCount = 1;
|
||||
}
|
||||
|
||||
var refIndex = Math.max(0, cards.length - colCount);
|
||||
var ref = cards[refIndex] || null;
|
||||
|
||||
if (trigger.parentNode) trigger.parentNode.removeChild(trigger);
|
||||
if (ref && ref.parentNode) {
|
||||
ref.parentNode.insertBefore(trigger, ref);
|
||||
} else {
|
||||
initGallery();
|
||||
grid.appendChild(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
if ('IntersectionObserver' in window) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) loadNextPage();
|
||||
});
|
||||
}, { root: null, rootMargin: LOAD_TRIGGER_MARGIN, threshold: 0 });
|
||||
// Place and observe the trigger. It will be moved after new cards are appended.
|
||||
placeTrigger();
|
||||
io.observe(trigger);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
relayout();
|
||||
onScrollOrResize();
|
||||
placeTrigger();
|
||||
}, { passive: true });
|
||||
window.addEventListener('scroll', onScrollOrResize, { passive: true });
|
||||
|
||||
relayout();
|
||||
placeTrigger();
|
||||
}
|
||||
|
||||
function init() {
|
||||
toArray(document.querySelectorAll('[data-nova-gallery]')).forEach(initOne);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<!-- Grid -->
|
||||
<section class="px-6 pb-10 md:px-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Card template (replace src with real thumbnails server-side) -->
|
||||
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
|
||||
<div class="aspect-[16/10] bg-gradient-to-br from-cyan-400/30 via-blue-500/20 to-purple-600/30">
|
||||
|
||||
@@ -16,6 +16,33 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
@vite(['resources/css/app.css','resources/scss/nova.scss','resources/js/nova.js'])
|
||||
<style>
|
||||
/* Gallery loading overlay */
|
||||
.nova-loader-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
}
|
||||
.nova-loader-spinner {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255,255,255,0.08);
|
||||
border-top-color: rgba(255,255,255,0.9);
|
||||
animation: novaSpin 0.9s linear infinite;
|
||||
box-shadow: 0 6px 18px rgba(2,6,23,0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
@keyframes novaSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Card enter animation */
|
||||
.nova-card-enter { opacity: 0; transform: translateY(10px) scale(0.995); }
|
||||
.nova-card-enter.nova-card-enter-active { transition: transform 380ms cubic-bezier(.2,.9,.2,1), opacity 380ms ease-out; opacity: 1; transform: none; }
|
||||
</style>
|
||||
@stack('head')
|
||||
</head>
|
||||
<body class="bg-nova-900 text-white min-h-screen flex flex-col">
|
||||
@@ -55,5 +82,6 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,62 +1,83 @@
|
||||
@php
|
||||
$img_src = $art->thumb ?? '';
|
||||
$img_srcset = $art->thumb_srcset ?? '';
|
||||
$base_height = 360;
|
||||
$orig_width = (int) ($art->width ?? 0);
|
||||
$orig_height = (int) ($art->height ?? 0);
|
||||
$img_height = $base_height;
|
||||
$img_width = ($orig_width > 0 && $orig_height > 0)
|
||||
? (int) round($orig_width * ($base_height / $orig_height))
|
||||
: 600;
|
||||
// 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 ?? 'Skinbase'));
|
||||
$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);
|
||||
|
||||
$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));
|
||||
|
||||
$contentUrl = $img_src;
|
||||
$cardUrl = (string) ($art->url ?? '#');
|
||||
@endphp
|
||||
|
||||
<article class="artwork" itemscope itemtype="http://schema.org/Photograph">
|
||||
<a href="{{ $art->url ?? '#' }}" itemprop="url" class="group relative rounded-2xl overflow-hidden bg-black/10 border border-white/6 shadow-sm hover:shadow-lg transition-shadow">
|
||||
<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 }}">
|
||||
|
||||
{{-- Category badge --}}
|
||||
@if(!empty($art->category_name))
|
||||
<div class="absolute top-3 left-3 z-30 bg-gradient-to-br from-black/65 to-black/40 text-xs text-white px-2 py-1 rounded-md backdrop-blur-sm">{{ $art->category_name }}</div>
|
||||
<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
|
||||
|
||||
{{-- Image container with subtle overlay; details hidden until hover --}}
|
||||
<div class="w-full h-48 md:h-56 lg:h-64 bg-neutral-900 relative overflow-hidden">
|
||||
<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, 25vw"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
alt="{{ e($art->name) }}"
|
||||
alt="{{ e($title) }}"
|
||||
width="{{ $img_width }}"
|
||||
height="{{ $img_height }}"
|
||||
class="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
|
||||
class="h-full w-full object-cover transition-transform duration-200 ease-out group-hover:scale-[1.04]"
|
||||
itemprop="thumbnailUrl"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
{{-- Hover overlay: hidden by default, slides up on hover --}}
|
||||
<div class="absolute inset-0 flex items-end pointer-events-none opacity-0 translate-y-3 group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div class="w-full bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
@if(!empty($art->author_avatar))
|
||||
<img src="{{ $art->author_avatar }}" alt="{{ $art->uname }}" class="w-8 h-8 rounded-full border border-white/8 object-cover">
|
||||
@else
|
||||
<span class="w-8 h-8 rounded-full bg-neutral-800 border border-white/6 flex items-center justify-center text-xs text-neutral-400">{{ strtoupper(substr($art->uname ?? '-',0,1)) }}</span>
|
||||
<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">by {{ $author }}</span>
|
||||
<span class="shrink-0">❤ {{ $likes }} · ⬇ {{ $downloads }}</span>
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] text-white/70">
|
||||
@if($resolution !== '')
|
||||
{{ $resolution }} •
|
||||
@endif
|
||||
|
||||
<div class="text-sm">
|
||||
<div class="text-white font-semibold leading-tight truncate">{{ $art->uname ?? '—' }}</div>
|
||||
<div class="text-neutral-300 text-xs truncate">{{ optional(data_get($art, 'created_at'))->format('M j, Y') ?? '' }}</div>
|
||||
{{ $category }} • {{ $license }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 text-xs text-neutral-300">
|
||||
<div class="flex items-center gap-1"><i class="fa fa-eye"></i><span class="ml-1">{{ $art->views ?? 0 }}</span></div>
|
||||
<div class="flex items-center gap-1"><i class="fa fa-heart"></i><span class="ml-1">{{ $art->likes ?? 0 }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Visually-hidden title for accessibility/SEO (details only shown on hover) --}}
|
||||
<span class="sr-only">{{ $art->name ?? 'Artwork' }}</span>
|
||||
<span class="sr-only">{{ $title }} by {{ $author }}</span>
|
||||
</a>
|
||||
</article>
|
||||
@@ -62,15 +62,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="px-6 pb-10 md:px-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<section class="px-6 pb-10 pt-8 md:px-10" data-nova-gallery data-gallery-type="browse">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" data-gallery-grid>
|
||||
@forelse ($artworks as $art)
|
||||
<a href="{{ $art->url }}" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
|
||||
<div class="aspect-[16/10] bg-neutral-900">
|
||||
<img src="{{ $art->thumb ?? '/images/placeholder.jpg' }}" srcset="{{ $art->thumb_srcset ?? ($art->thumb ?? '/images/placeholder.jpg') }}" alt="{{ $art->name ?? 'Artwork' }}" loading="lazy" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">{{ $art->name ?? 'Artwork' }}</div>
|
||||
</a>
|
||||
@include('legacy._artwork_card', ['art' => $art])
|
||||
@empty
|
||||
<div class="panel panel-default effect2">
|
||||
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
|
||||
@@ -81,9 +76,10 @@
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-10">
|
||||
<div class="flex justify-center mt-10" data-gallery-pagination>
|
||||
{{ $artworks->withQueryString()->links() }}
|
||||
</div>
|
||||
<div class="hidden mt-8" data-gallery-skeleton></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
@@ -97,5 +93,39 @@
|
||||
.nb-hero-fade {
|
||||
background: linear-gradient(180deg, rgba(17,24,39,0) 0%, rgba(7,10,15,0.9) 60%, rgba(7,10,15,1) 100%);
|
||||
}
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
grid-auto-rows: 8px;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
/* From 768px up to ~991px show 2 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 2600px) {
|
||||
/* Very large displays: 5 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
}
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
|
||||
[data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; }
|
||||
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
|
||||
.nova-skeleton-card {
|
||||
border-radius: 1rem;
|
||||
min-height: 180px;
|
||||
background: linear-gradient(110deg, rgba(255,255,255,.06) 8%, rgba(255,255,255,.12) 18%, rgba(255,255,255,.06) 33%);
|
||||
background-size: 200% 100%;
|
||||
animation: novaShimmer 1.2s linear infinite;
|
||||
}
|
||||
@keyframes novaShimmer {
|
||||
to { background-position-x: -200%; }
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="/js/legacy-gallery-init.js" defer></script>
|
||||
@endpush
|
||||
|
||||
@@ -76,15 +76,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<section class="px-6 pb-10 md:px-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<section class="px-6 pb-10 md:px-10" data-nova-gallery data-gallery-type="category-slug">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" data-gallery-grid>
|
||||
@forelse ($artworks as $art)
|
||||
<a href="{{ $art->url }}" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
|
||||
<div class="aspect-[16/10] bg-neutral-900">
|
||||
<img src="{{ $art->thumbnail_url ?? '/images/placeholder.jpg' }}" alt="{{ $art->title ?? 'Artwork' }}" loading="lazy" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">{{ $art->title ?? 'Artwork' }}</div>
|
||||
</a>
|
||||
@include('legacy._artwork_card', ['art' => $art])
|
||||
@empty
|
||||
<div class="panel panel-default effect2">
|
||||
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
|
||||
@@ -95,11 +90,12 @@
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-10">
|
||||
<div class="flex justify-center mt-10" data-gallery-pagination>
|
||||
@if ($artworks instanceof \Illuminate\Contracts\Pagination\Paginator)
|
||||
{{ $artworks->links() }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="hidden mt-8" data-gallery-skeleton></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
@@ -110,32 +106,35 @@
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.gallery {
|
||||
columns: 5 260px;
|
||||
column-gap: 16px;
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
grid-auto-rows: 8px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.artwork {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 16px;
|
||||
@media (min-width: 768px) {
|
||||
/* From 768px up to ~991px show 2 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.gallery {
|
||||
columns: 3 220px;
|
||||
@media (min-width: 1024px) {
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 2600px) {
|
||||
/* Very large displays: 5 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gallery {
|
||||
columns: 2 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.gallery {
|
||||
columns: 1 100%;
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
|
||||
[data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; }
|
||||
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
|
||||
.nova-skeleton-card {
|
||||
border-radius: 1rem;
|
||||
min-height: 180px;
|
||||
background: linear-gradient(110deg, rgba(255,255,255,.06) 8%, rgba(255,255,255,.12) 18%, rgba(255,255,255,.06) 33%);
|
||||
background-size: 200% 100%;
|
||||
animation: novaShimmer 1.2s linear infinite;
|
||||
}
|
||||
@keyframes novaShimmer {
|
||||
to { background-position-x: -200%; }
|
||||
}
|
||||
.nb-hero-fade {
|
||||
background: linear-gradient(180deg, rgba(17,24,39,0) 0%, rgba(7,10,15,0.9) 60%, rgba(7,10,15,1) 100%);
|
||||
@@ -143,3 +142,7 @@
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="/js/legacy-gallery-init.js" defer></script>
|
||||
@endpush
|
||||
|
||||
@@ -89,15 +89,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Artworks gallery (same layout as subcategory pages) -->
|
||||
<section class="px-6 pb-10 md:px-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<section class="px-6 pb-10 md:px-10" data-nova-gallery data-gallery-type="content-type">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" data-gallery-grid>
|
||||
@forelse ($artworks as $art)
|
||||
<a href="{{ $art->url }}" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
|
||||
<div class="aspect-[16/10] bg-neutral-900">
|
||||
<img src="{{ $art->thumbnail_url ?? '/images/placeholder.jpg' }}" alt="{{ $art->title ?? 'Artwork' }}" loading="lazy" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">{{ $art->title ?? 'Artwork' }}</div>
|
||||
</a>
|
||||
@include('legacy._artwork_card', ['art' => $art])
|
||||
@empty
|
||||
<div class="panel panel-default effect2">
|
||||
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
|
||||
@@ -108,9 +103,10 @@
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-10">
|
||||
<div class="flex justify-center mt-10" data-gallery-pagination>
|
||||
{{ $artworks->withQueryString()->links('pagination::tailwind') }}
|
||||
</div>
|
||||
<div class="hidden mt-8" data-gallery-skeleton></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
@@ -121,13 +117,42 @@
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.gallery { columns: 5 260px; column-gap: 16px; }
|
||||
.artwork { break-inside: avoid; margin-bottom: 16px; }
|
||||
@media (max-width: 992px) { .gallery { columns: 3 220px; } }
|
||||
@media (max-width: 768px) { .gallery { columns: 2 180px; } }
|
||||
@media (max-width: 576px) { .gallery { columns: 1 100%; } }
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
grid-auto-rows: 8px;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
/* From 768px up to ~991px show 2 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (min-width: 2600px) {
|
||||
/* Very large displays: 5 columns */
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
}
|
||||
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
|
||||
[data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; }
|
||||
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
|
||||
.nova-skeleton-card {
|
||||
border-radius: 1rem;
|
||||
min-height: 180px;
|
||||
background: linear-gradient(110deg, rgba(255,255,255,.06) 8%, rgba(255,255,255,.12) 18%, rgba(255,255,255,.06) 33%);
|
||||
background-size: 200% 100%;
|
||||
animation: novaShimmer 1.2s linear infinite;
|
||||
}
|
||||
@keyframes novaShimmer {
|
||||
to { background-position-x: -200%; }
|
||||
}
|
||||
.nb-hero-fade {
|
||||
background: linear-gradient(180deg, rgba(17,24,39,0) 0%, rgba(7,10,15,0.9) 60%, rgba(7,10,15,1) 100%);
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="/js/legacy-gallery-init.js" defer></script>
|
||||
@endpush
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<!-- MAIN -->
|
||||
<main class="flex-1">
|
||||
<div class="nova-gallery grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
<div class="nova-gallery grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
@foreach($artworks as $art)
|
||||
@include('legacy._artwork_card', ['art' => $art])
|
||||
@endforeach
|
||||
|
||||
Reference in New Issue
Block a user