
2026 Photography Competition
<!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>