photos.html

← Back to explorer
templates/photos.html
{% extends "base.html" %}
{% block title %}Photos · Kishaloy Roy{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/photos_page.css') }}">
{% endblock %}

{% block content %}
  <h1 class="page-title">My Photo Gallery</h1>
  <p class="page-desc">Some Photos I Clicked.</p>

  {% if photos|length == 0 %}
    <p class="empty">No photos yet. Upload via <a href="{{ url_for('admin_photos') }}">admin panel</a>.</p>
  {% else %}
    <section class="gallery" aria-label="Photo gallery">
      {% for p in photos %}
        <button
          class="tile"
          data-index="{{ loop.index0 }}"
          data-src="{{ p.url }}"
          data-original="{{ p.original_url or '' }}"
          data-title="{{ p.title or '' }}"
          data-camera="{{ p.camera or '' }}"
          data-lens="{{ p.lens or '' }}"
          data-focal="{{ p.focal_length or '' }}"
          data-aperture="{{ p.aperture or '' }}"
          data-shutter="{{ p.shutter_speed or '' }}"
          data-iso="{{ p.iso or '' }}"
          data-date="{{ p.date_taken or '' }}"
          aria-label="{{ p.title or 'Photo ' ~ loop.index }}"
        >
          <img src="{{ p.url }}" alt="{{ p.title or 'Photo ' ~ loop.index }}" loading="lazy">
        </button>
      {% endfor %}
    </section>

    <!-- Lightbox -->
    <div class="lb-overlay" id="lb" role="dialog" aria-modal="true" aria-label="Photo viewer">

      <!-- Top-right toolbar -->
      <div class="lb-toolbar">
        <a class="lb-btn lb-load-orig" id="lb-orig" href="#" target="_blank" rel="noreferrer" style="display:none;">Load original</a>
        <button class="lb-btn lb-info-btn" id="lb-info-btn" aria-label="Toggle info" aria-pressed="false">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
            <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.4"/>
            <rect x="7.2" y="6.8" width="1.6" height="5.2" rx="0.8" fill="currentColor"/>
            <circle cx="8" cy="4.4" r="0.9" fill="currentColor"/>
          </svg>
        </button>
        <button class="lb-btn lb-close-btn" id="lb-close" aria-label="Close">
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
            <path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
          </svg>
        </button>
      </div>

      <!-- Main stage: image + sliding meta panel -->
      <div class="lb-stage" id="lb-stage">
        <button class="lb-nav lb-prev" id="lb-prev" aria-label="Previous">&#8249;</button>

        <div class="lb-img-wrap">
          <img class="lb-img" id="lb-img" src="" alt="">
        </div>

        <button class="lb-nav lb-next" id="lb-next" aria-label="Next">&#8250;</button>

        <!-- Metadata panel — slides in from right, beside the image -->
        <aside class="lb-meta-panel" id="lb-meta-panel" aria-label="Photo information">
          <div class="lb-meta-scroll">
            <p class="lb-meta-title" id="lb-meta-title"></p>
            <div id="lb-meta-list"></div>
          </div>
        </aside>
      </div>

      <div class="lb-counter" id="lb-counter"></div>
    </div>
  {% endif %}
{% endblock %}

{% block scripts %}
{% if photos|length > 0 %}
<script>
(function () {
  const tiles   = Array.from(document.querySelectorAll('.tile'));
  const lb      = document.getElementById('lb');
  const lbStage = document.getElementById('lb-stage');
  const lbImg   = document.getElementById('lb-img');
  const lbPrev  = document.getElementById('lb-prev');
  const lbNext  = document.getElementById('lb-next');
  const lbClose = document.getElementById('lb-close');
  const lbOrig  = document.getElementById('lb-orig');
  const lbCount = document.getElementById('lb-counter');
  const lbPanel = document.getElementById('lb-meta-panel');
  const lbInfo  = document.getElementById('lb-info-btn');
  const lbTitle = document.getElementById('lb-meta-title');
  const lbList  = document.getElementById('lb-meta-list');
  let current = 0;
  let metaOpen = false;

  const FIELDS = [
    { label: 'Camera',        key: 'camera'   },
    { label: 'Lens',          key: 'lens'     },
    { label: 'Focal Length',  key: 'focal'    },
    { label: 'Aperture',      key: 'aperture' },
    { label: 'Shutter Speed', key: 'shutter'  },
    { label: 'ISO',           key: 'iso'      },
    { label: 'Date',          key: 'date'     },
  ];

  function openAt(idx) {
    current = idx;
    lb.classList.add('lb-open');
    document.body.style.overflow = 'hidden';
    render();
  }

  function close() {
    lb.classList.remove('lb-open');
    document.body.style.overflow = '';
    setMeta(false);
  }

  function setMeta(open) {
    metaOpen = open;
    lbPanel.classList.toggle('meta-open', open);
    lbStage.classList.toggle('stage-meta-open', open);
    lbInfo.setAttribute('aria-pressed', open ? 'true' : 'false');
    lbInfo.classList.toggle('lb-info-active', open);
  }

  function render() {
    const tile  = tiles[current];
    const total = tiles.length;

    lbImg.classList.add('lb-loading');
    const preload = new Image();
    preload.onload = () => { lbImg.src = preload.src; lbImg.classList.remove('lb-loading'); };
    preload.src = tile.dataset.src;
    lbImg.alt = tile.dataset.title || '';

    lbCount.textContent = `${current + 1} / ${total}`;

    lbPrev.disabled = current === 0;
    lbNext.disabled = current === total - 1;
    lbPrev.classList.toggle('lb-nav-off', current === 0);
    lbNext.classList.toggle('lb-nav-off', current === total - 1);

    const origUrl = tile.dataset.original;
    lbOrig.style.display = origUrl ? '' : 'none';
    if (origUrl) lbOrig.href = origUrl;

    const title = tile.dataset.title;
    lbTitle.textContent = title || '';
    lbTitle.style.display = title ? 'block' : 'none';

    const rows = FIELDS
      .filter(f => tile.dataset[f.key])
      .map(f => `<div class="meta-row"><span class="meta-label">${f.label}</span><span class="meta-value">${tile.dataset[f.key]}</span></div>`)
      .join('');
    lbList.innerHTML = rows || '<span class="meta-empty">No metadata available.</span>';

    const hasAnyMeta = title || FIELDS.some(f => tile.dataset[f.key]);
    lbInfo.style.display = hasAnyMeta ? 'flex' : 'none';
    if (!hasAnyMeta) setMeta(false);
  }

  tiles.forEach(t => t.addEventListener('click', () => openAt(+t.dataset.index)));

  lbPrev.addEventListener('click', e => { e.stopPropagation(); if (current > 0) { current--; render(); } });
  lbNext.addEventListener('click', e => { e.stopPropagation(); if (current < tiles.length - 1) { current++; render(); } });

  lbClose.addEventListener('click', close);
  lb.addEventListener('click', e => { if (e.target === lb) close(); });

  lbInfo.addEventListener('click', e => { e.stopPropagation(); setMeta(!metaOpen); });
  lbPanel.addEventListener('click', e => e.stopPropagation());

  document.addEventListener('keydown', e => {
    if (!lb.classList.contains('lb-open')) return;
    if (e.key === 'Escape')     close();
    if (e.key === 'ArrowLeft'  && current > 0)                 { current--; render(); }
    if (e.key === 'ArrowRight' && current < tiles.length - 1)  { current++; render(); }
  });
})();
</script>
{% endif %}
{% endblock %}