feat: increase gallery grid from 4 to 5 columns per row on desktopfeat: increase gallery grid from 4 to 5 columns per row on desktop
This commit is contained in:
124
resources/js/lib/nav-context.js
Normal file
124
resources/js/lib/nav-context.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Nova Gallery Navigation Context
|
||||
*
|
||||
* Stores artwork list context in sessionStorage when a card is clicked,
|
||||
* so the artwork page can provide prev/next navigation without page reload.
|
||||
*
|
||||
* Context shape:
|
||||
* { source, key, ids: number[], index: number, page: string, ts: number }
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'nav_ctx';
|
||||
|
||||
function getPageContext() {
|
||||
var path = window.location.pathname;
|
||||
var search = window.location.search;
|
||||
|
||||
// /tag/{slug}
|
||||
var tagMatch = path.match(/^\/tag\/([^/]+)\/?$/);
|
||||
if (tagMatch) return { source: 'tag', key: 'tag:' + tagMatch[1] };
|
||||
|
||||
// /browse/{contentType}/{category...}
|
||||
var browseMatch = path.match(/^\/browse\/([^/]+)(?:\/(.+))?\/?$/);
|
||||
if (browseMatch) {
|
||||
var browsePart = browseMatch[1] + (browseMatch[2] ? '/' + browseMatch[2] : '');
|
||||
return { source: 'browse', key: 'browse:' + browsePart };
|
||||
}
|
||||
|
||||
// /search?q=...
|
||||
if (path === '/search' || path.startsWith('/search?')) {
|
||||
var q = new URLSearchParams(search).get('q') || '';
|
||||
return { source: 'search', key: 'search:' + q };
|
||||
}
|
||||
|
||||
// /@{username}
|
||||
var profileMatch = path.match(/^\/@([^/]+)\/?$/);
|
||||
if (profileMatch) return { source: 'profile', key: 'profile:' + profileMatch[1] };
|
||||
|
||||
// /members/...
|
||||
if (path.startsWith('/members')) return { source: 'members', key: 'members' };
|
||||
|
||||
// home
|
||||
if (path === '/' || path === '/home') return { source: 'home', key: 'home' };
|
||||
|
||||
return { source: 'page', key: 'page:' + path };
|
||||
}
|
||||
|
||||
function collectIds() {
|
||||
var cards = document.querySelectorAll('article[data-art-id]');
|
||||
var ids = [];
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var raw = cards[i].getAttribute('data-art-id');
|
||||
var id = parseInt(raw, 10);
|
||||
if (id > 0 && !isNaN(id)) ids.push(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function saveContext(artId, ids, context) {
|
||||
var index = ids.indexOf(artId);
|
||||
if (index === -1) index = 0;
|
||||
var ctx = {
|
||||
source: context.source,
|
||||
key: context.key,
|
||||
ids: ids,
|
||||
index: index,
|
||||
page: window.location.href,
|
||||
ts: Date.now(),
|
||||
};
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(ctx));
|
||||
} catch (_) {
|
||||
// quota exceeded or private mode — silently skip
|
||||
}
|
||||
}
|
||||
|
||||
function findArticle(el) {
|
||||
var node = el;
|
||||
while (node && node !== document.body) {
|
||||
if (node.tagName === 'ARTICLE' && node.hasAttribute('data-art-id')) {
|
||||
return node;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function init() {
|
||||
// Only act on pages that have artwork cards (not the artwork detail page itself)
|
||||
var cards = document.querySelectorAll('article[data-art-id]');
|
||||
if (cards.length === 0) return;
|
||||
|
||||
// Don't inject on the artwork detail page (has #artwork-page mount)
|
||||
if (document.getElementById('artwork-page')) return;
|
||||
|
||||
var context = getPageContext();
|
||||
|
||||
document.addEventListener(
|
||||
'click',
|
||||
function (event) {
|
||||
var article = findArticle(event.target);
|
||||
if (!article) return;
|
||||
|
||||
// Make sure click was on or inside the card's <a> link
|
||||
var link = article.querySelector('a[href]');
|
||||
if (!link) return;
|
||||
|
||||
var artId = parseInt(article.getAttribute('data-art-id'), 10);
|
||||
if (!artId || isNaN(artId)) return;
|
||||
|
||||
var currentIds = collectIds();
|
||||
saveContext(artId, currentIds, context);
|
||||
},
|
||||
true // capture phase: store before navigation fires
|
||||
);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
43
resources/js/lib/useNavContext.js
Normal file
43
resources/js/lib/useNavContext.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* useNavContext
|
||||
*
|
||||
* Provides prev/next artwork IDs scoped to the same author via API.
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// Module-level cache for API calls
|
||||
const fallbackCache = new Map();
|
||||
|
||||
async function fetchFallback(artworkId) {
|
||||
const key = String(artworkId);
|
||||
if (fallbackCache.has(key)) return fallbackCache.get(key);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/artworks/navigation/${artworkId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!res.ok) return { prevId: null, nextId: null, prevUrl: null, nextUrl: null };
|
||||
const data = await res.json();
|
||||
const result = {
|
||||
prevId: data.prev_id ?? null,
|
||||
nextId: data.next_id ?? null,
|
||||
prevUrl: data.prev_url ?? null,
|
||||
nextUrl: data.next_url ?? null,
|
||||
};
|
||||
fallbackCache.set(key, result);
|
||||
return result;
|
||||
} catch {
|
||||
return { prevId: null, nextId: null, prevUrl: null, nextUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function useNavContext(currentArtworkId) {
|
||||
/**
|
||||
* Always resolve via API to guarantee same-user navigation.
|
||||
*/
|
||||
const getNeighbors = useCallback(async () => {
|
||||
return fetchFallback(currentArtworkId);
|
||||
}, [currentArtworkId]);
|
||||
|
||||
return { getNeighbors };
|
||||
}
|
||||
Reference in New Issue
Block a user