Gallery

Gallery

2026 Photography Competition

Newsletter

Got questions or want to get involved?

Reach out to us at here

Follow us on our Socials

Proudly sponsored by

© 2024 All Rights Reserved

Newsletter

Got questions or want to get involved?

Reach out to us at here

Follow us on our Socials

Proudly sponsored by

© 2024 All Rights Reserved

Newsletter

Got questions or want to get involved?

Reach out to us at here

Follow us on our Socials

Proudly sponsored by

© 2024 All Rights Reserved

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>TMF Photo Gallery</title>

<style>

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }


html, body {

width: 100%;

background: transparent;

color: #ffffff;

font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

font-size: 14px;

}


/* Force everything to stack vertically, no matter what Framer does */

.wrap {

display: block;

width: 100%;

}


/* ── Filter bar ── */

.filter-bar {

display: block;

width: 100%;

padding-bottom: 20px;

margin-bottom: 20px;

border-bottom: 1px solid rgba(255,255,255,0.12);

}


.filter-inner {

display: flex;

flex-direction: row;

flex-wrap: wrap;

gap: 8px;

align-items: center;

}


.filter-label {

font-size: 11px;

color: rgba(255,255,255,0.4);

letter-spacing: 0.15em;

text-transform: uppercase;

margin-right: 4px;

}


.filter-btn {

background: transparent;

border: 1px solid rgba(255,255,255,0.2);

color: rgba(255,255,255,0.5);

font-family: inherit;

font-size: 12px;

letter-spacing: 0.06em;

padding: 6px 18px;

border-radius: 100px;

cursor: pointer;

transition: all 0.2s;

}


.filter-btn:hover {

border-color: rgba(255,255,255,0.5);

color: rgba(255,255,255,0.8);

}


.filter-btn.active {

background: #ffffff;

border-color: #ffffff;

color: #000000;

}


/* ── Gallery grid ── */

#gallery {

display: grid;

grid-template-columns: repeat(3, 1fr);

gap: 3px;

width: 100%;

}


.card {

position: relative;

aspect-ratio: 1;

overflow: hidden;

cursor: pointer;

background: #1a1a1a;

}


.card img {

width: 100%;

height: 100%;

object-fit: cover;

display: block;

transition: transform 0.5s cubic-bezier(0.25, 0, 0, 1);

}


.card:hover img { transform: scale(1.05); }


.card-overlay {

position: absolute;

inset: 0;

background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 50%);

opacity: 0;

transition: opacity 0.3s ease;

display: flex;

align-items: flex-end;

padding: 14px;

}


.card:hover .card-overlay { opacity: 1; }


.card-name {

font-size: 13px;

font-weight: 500;

color: #fff;

display: block;

line-height: 1.3;

}


.card-photographer {

font-size: 11px;

color: rgba(255,255,255,0.6);

margin-top: 2px;

display: block;

}


/* Always-visible name below card */

.card-label {

padding: 8px 4px 0;

font-size: 12px;

color: rgba(255,255,255,0.7);

}


/* ── Skeleton ── */

.skeleton {

background: #1a1a1a;

position: relative;

overflow: hidden;

aspect-ratio: 1;

}


.skeleton::after {

content: '';

position: absolute;

inset: 0;

background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);

animation: shimmer 1.6s infinite;

}


@keyframes shimmer {

0% { transform: translateX(-100%); }

100% { transform: translateX(100%); }

}


/* ── Empty / error ── */

#state {

display: block;

width: 100%;

padding: 60px 24px;

text-align: center;

color: rgba(255,255,255,0.4);

}


#state strong {

display: block;

font-size: 1.1rem;

font-weight: 500;

color: #fff;

margin-bottom: 8px;

}


/* ── Lightbox ── */

.lightbox {

display: none;

position: fixed;

inset: 0;

background: rgba(0,0,0,0.94);

z-index: 9999;

align-items: center;

justify-content: center;

padding: 32px;

}


.lightbox.open { display: flex; }


.lightbox-inner {

display: flex;

gap: 40px;

max-width: 1000px;

width: 100%;

max-height: 90vh;

align-items: center;

}


.lightbox-img-wrap { flex: 1; min-width: 0; }


.lightbox-img-wrap img {

width: 100%;

max-height: 80vh;

object-fit: contain;

display: block;

}


.lightbox-details { width: 220px; flex-shrink: 0; }


.lightbox-details h2 {

font-size: 1.3rem;

font-weight: 500;

color: #fff;

margin-bottom: 8px;

line-height: 1.3;

}


.lightbox-details .photographer {

font-size: 13px;

color: rgba(255,255,255,0.5);

margin-bottom: 6px;

}


.lightbox-details .photographer span { color: #fff; }


.lightbox-details .cat {

font-size: 10px;

color: rgba(255,255,255,0.4);

letter-spacing: 0.12em;

text-transform: uppercase;

margin-bottom: 16px;

}


.lightbox-details .desc {

font-size: 13px;

color: rgba(255,255,255,0.5);

line-height: 1.7;

}


.lightbox-close {

position: fixed;

top: 20px; right: 24px;

background: none;

border: none;

color: rgba(255,255,255,0.5);

font-size: 28px;

cursor: pointer;

line-height: 1;

transition: color 0.2s;

}


.lightbox-close:hover { color: #fff; }


@media (max-width: 600px) {

#gallery { grid-template-columns: repeat(2, 1fr); }

.lightbox-inner { flex-direction: column; }

.lightbox-details { width: 100%; }

}

</style>

</head>

<body>

<div class="wrap">


<div class="filter-bar">

<div class="filter-inner">

<span class="filter-label">Filter</span>

<button class="filter-btn active" data-filter="all">All</button>

<button class="filter-btn" data-filter="Adult">Adult</button>

<button class="filter-btn" data-filter="Youth">Youth</button>

</div>

</div>


<div id="gallery"></div>

<div id="state" style="display:none;"></div>


</div>


<div class="lightbox" id="lightbox">

<button class="lightbox-close" id="lightbox-close">✕</button>

<div class="lightbox-inner">

<div class="lightbox-img-wrap">

<img id="lb-img" src="" alt="" />

</div>

<div class="lightbox-details">

<h2 id="lb-title"></h2>

<p class="photographer">By <span id="lb-name"></span></p>

<p class="cat" id="lb-category"></p>

<p class="desc" id="lb-desc"></p>

</div>

</div>

</div>


<script>

const SHEET_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTbqzDvn0VvVhP7fOtsi6CwcRspiANxXdM57oQskycQNKwH42sfAAb5qwYvve3Ik2lS795ndA6ka9U1/pub?output=csv';


let allEntries = [];


// Auto-convert Google Drive share URLs to direct image URLs

function toDirectImageUrl(url) {

if (!url) return '';

// Already a direct URL

if (url.includes('uc?export=view')) return url;

// Convert /file/d/ID/view → uc?export=view&id=ID

const match = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);

if (match) return `https://drive.google.com/uc?export=view&id=${match[1]}`;

return url;

}


function parseCSV(text) {

const lines = text.trim().split('\n');

const headers = parseCSVRow(lines[0]);

return lines.slice(1).map(line => {

const vals = parseCSVRow(line);

const obj = {};

headers.forEach((h, i) => obj[h.trim()] = (vals[i] || '').trim());

return obj;

}).filter(r => r.title || r.photographer_name || r.image_url);

}


function parseCSVRow(row) {

const result = [];

let current = '', inQuotes = false;

for (let i = 0; i < row.length; i++) {

if (row[i] === '"') {

if (inQuotes && row[i+1] === '"') { current += '"'; i++; }

else inQuotes = !inQuotes;

} else if (row[i] === ',' && !inQuotes) {

result.push(current); current = '';

} else { current += row[i]; }

}

result.push(current);

return result;

}


function renderGallery(entries) {

const gallery = document.getElementById('gallery');

const state = document.getElementById('state');

gallery.innerHTML = '';


if (!entries.length) {

gallery.style.display = 'none';

state.style.display = 'block';

state.innerHTML = '<strong>No entries yet</strong>Check back soon — photos will appear here as entries are approved.';

return;

}


gallery.style.display = 'grid';

state.style.display = 'none';


entries.forEach(entry => {

const imgUrl = toDirectImageUrl(entry.image_url);


const card = document.createElement('div');

card.className = 'card';


const img = document.createElement('img');

img.src = imgUrl;

img.alt = entry.title || entry.photographer_name || 'Photo entry';

img.loading = 'lazy';

img.onerror = () => { img.style.opacity = '0'; };


const overlay = document.createElement('div');

overlay.className = 'card-overlay';

overlay.innerHTML = `<div>

<span class="card-name">${entry.title || 'Untitled'}</span>

<span class="card-photographer">${entry.photographer_name || ''}</span>

</div>`;


card.appendChild(img);

card.appendChild(overlay);

card.addEventListener('click', () => openLightbox(entry, imgUrl));

gallery.appendChild(card);

});

}


function openLightbox(entry, imgUrl) {

document.getElementById('lb-img').src = imgUrl || '';

document.getElementById('lb-img').alt = entry.title || '';

document.getElementById('lb-title').textContent = entry.title || 'Untitled';

document.getElementById('lb-name').textContent = entry.photographer_name || '—';

document.getElementById('lb-category').textContent = entry.category || '';

document.getElementById('lb-desc').textContent = entry.description || '';

document.getElementById('lightbox').classList.add('open');

}


document.getElementById('lightbox-close').addEventListener('click', () => {

document.getElementById('lightbox').classList.remove('open');

});

document.getElementById('lightbox').addEventListener('click', e => {

if (e.target === document.getElementById('lightbox'))

document.getElementById('lightbox').classList.remove('open');

});

document.addEventListener('keydown', e => {

if (e.key === 'Escape') document.getElementById('lightbox').classList.remove('open');

});


document.querySelectorAll('.filter-btn').forEach(btn => {

btn.addEventListener('click', () => {

document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));

btn.classList.add('active');

const f = btn.dataset.filter;

renderGallery(f === 'all' ? allEntries : allEntries.filter(e => e.category === f));

});

});


function showSkeletons() {

const g = document.getElementById('gallery');

g.innerHTML = '';

g.style.display = 'grid';

for (let i = 0; i < 6; i++) {

const sk = document.createElement('div');

sk.className = 'skeleton';

g.appendChild(sk);

}

}


async function loadGallery() {

showSkeletons();

try {

const res = await fetch(SHEET_CSV_URL);

if (!res.ok) throw new Error('Sheet not accessible');

allEntries = parseCSV(await res.text());

renderGallery(allEntries);

} catch (err) {

document.getElementById('gallery').style.display = 'none';

const s = document.getElementById('state');

s.style.display = 'block';

s.innerHTML = '<strong>Gallery unavailable</strong>Unable to load photos right now. Please try again shortly.';

}

}


loadGallery();

</script>

</body>

</html>