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:
2026-02-25 19:11:23 +01:00
parent 5c97488e80
commit 0032aec02f
131 changed files with 15674 additions and 597 deletions

View 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();
}
})();

View 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 };
}