lab-js/gallery.jsjavascript
/** Public, read-only shared gallery loader. */
import { t } from './i18n.js';

import { backendOrigin, PRODUCTION_ORIGIN } from './backend-url.js';
export { PRODUCTION_ORIGIN } from './backend-url.js';
export const GALLERY_PATH = '/api/gallery?limit=24';

export function initialGalleryOrigin(locationLike = window.location) {
  return backendOrigin(locationLike);
}

export function galleryUrl(origin) {
  return new URL(GALLERY_PATH, origin).href;
}

function safeHttpUrl(value) {
  try {
    const url = new URL(value);
    return ['http:', 'https:'].includes(url.protocol) ? url.href : null;
  } catch {
    return null;
  }
}

export async function fetchGallery(origin, fetchImpl = fetch) {
  const response = await fetchImpl(galleryUrl(origin), { headers: { Accept: 'application/json' } });
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
  const body = await response.json();
  if (!body || body.ok !== true || !Array.isArray(body.items)) throw new Error('Invalid gallery response');
  return body.items;
}

function renderItems(container, items) {
  container.replaceChildren();
  if (!items.length) {
    const empty = document.createElement('p');
    empty.className = 'gallery-status';
    empty.textContent = t('gallery_empty_status');
    container.append(empty);
    return;
  }

  for (const item of items) {
    const imageUrl = safeHttpUrl(item.imageUrl);
    const postUrl = safeHttpUrl(item.postUrl);
    if (!imageUrl || !postUrl) continue;
    const card = document.createElement('a');
    card.className = 'gallery-card';
    card.href = postUrl;
    card.target = '_blank';
    card.rel = 'noopener noreferrer';

    const image = document.createElement('img');
    image.src = safeHttpUrl(item.thumbnailUrl) || imageUrl;
    image.alt = item.alt || item.content || t('gallery_image_alt');
    image.loading = 'lazy';
    image.decoding = 'async';

    const caption = document.createElement('span');
    caption.textContent = item.content || item.ghostyleId || t('gallery_image_alt');
    card.append(image, caption);
    container.append(card);
  }
}

export function initGallery() {
  const container = document.getElementById('gm-gallery-grid');
  const status = document.getElementById('gm-gallery-status');
  const fallback = document.getElementById('gm-gallery-fallback');
  if (!container || !status || !fallback) return;

  const localOrigin = initialGalleryOrigin();

  async function load(origin, allowFallback) {
    status.hidden = false;
    status.textContent = t('gallery_loading_status');
    fallback.hidden = true;
    try {
      const items = await fetchGallery(origin);
      renderItems(container, items);
      status.hidden = true;
    } catch (_) {
      container.replaceChildren();
      status.textContent = t('gallery_load_failed_status');
      fallback.hidden = !allowFallback;
    }
  }

  fallback.addEventListener('click', () => load(PRODUCTION_ORIGIN, false));
  load(localOrigin, localOrigin !== PRODUCTION_ORIGIN);
}