← back to Sdcc Images
server.js
182 lines
/**
* SDCC Images — local admin-only gallery for Steve's Student Debt Crisis images.
* - Source: Steve's personal Drive (steve-personal account via George tailnet)
* - Originals stay in Drive — viewer embeds Drive thumbnails + links to originals
* - Basic Auth: admin / DWSecure2024!
* - 10 themed views, one per Theme Factory preset
*
* Routes:
* GET / → theme picker (lists all 10)
* GET /theme/:slug → themed gallery
* GET /api/files → JSON of files
* GET /api/themes → JSON of themes
*
* Port: 9824 (moved from 9821 to avoid collision with stayclaim/pastdoor)
*/
const express = require('express');
const helmet = require('helmet');
const basicAuth = require('express-basic-auth');
const path = require('path');
const fs = require('fs');
const themes = require('./themes');
const PORT = 9824;
const DATA_FILE = path.join(__dirname, 'data', 'files.json');
const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
// Public (no-auth) routes for Figma capture flow — only render themed pages with capture script
app.get('/capture/:slug', (req, res) => {
const html = renderTheme(req.params.slug, /*captureMode*/ true);
if (!html) return res.status(404).send('Unknown theme');
res.set('Content-Type','text/html').send(html);
});
// Everything else requires admin auth
app.use(basicAuth({
users: { 'admin': 'DWSecure2024!' },
challenge: true,
realm: 'SDCC Images',
}));
app.use(express.static(path.join(__dirname, 'public')));
function loadFiles() {
if (!fs.existsSync(DATA_FILE)) return { files: [], generatedAt: null, query: '', fileCount: 0 };
try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch { return { files: [], generatedAt: null, fileCount: 0 }; }
}
app.get('/api/files', (_req, res) => res.json(loadFiles()));
app.get('/api/themes', (_req, res) => res.json(themes));
function htmlEsc(s) { return String(s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function renderIndex() {
const data = loadFiles();
const cards = Object.entries(themes).map(([slug, t]) => `
<a class="card" href="/theme/${slug}" style="background:${t.palette.bg};color:${t.palette.text};font-family:${t.fontBody}">
<div class="swatches">
<span style="background:${t.palette.bg}"></span>
<span style="background:${t.palette.accent}"></span>
<span style="background:${t.palette.accent2}"></span>
<span style="background:${t.palette.text}"></span>
</div>
<h3 style="font-family:${t.fontHead};font-weight:${t.weightHead};color:${t.palette.accent}">${t.name}</h3>
<p style="opacity:.85">${t.desc}</p>
<span class="cta" style="border-color:${t.palette.accent};color:${t.palette.accent}">Open gallery →</span>
</a>
`).join('');
return `<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>SDCC Images — Theme Picker</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; background: #0a0c12; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
header { margin-bottom: 28px; }
header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 700; }
header p { margin: 0; color: #9ca3af; font-size: 13px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.card { display: block; padding: 22px; border-radius: 14px; text-decoration: none; transition: transform .15s, box-shadow .15s; box-shadow: 0 1px 3px rgba(0,0,0,.4); border: 1px solid rgba(255,255,255,.08); }
.card:hover { transform: translateY(-3px); box-shadow: 0 12px 30px rgba(0,0,0,.6); }
.card h3 { margin: 0 0 6px; font-size: 22px; }
.card p { margin: 0 0 16px; font-size: 13px; }
.swatches { display: flex; gap: 4px; margin-bottom: 14px; }
.swatches span { width: 24px; height: 24px; border-radius: 50%; border: 2px solid rgba(255,255,255,.25); }
.cta { display: inline-block; padding: 6px 12px; border: 1px solid; border-radius: 999px; font-size: 12px; font-weight: 600; }
.meta { margin-top: 30px; padding-top: 18px; border-top: 1px solid #1a1d27; color: #6b7280; font-size: 12px; }
</style></head>
<body>
<header>
<h1>SDCC Images — Themed Galleries</h1>
<p>${data.fileCount || data.files.length} images indexed from Steve's Google Drive · pick a theme to view</p>
</header>
<div class="grid">${cards}</div>
<div class="meta">
Last refresh: ${data.generatedAt ? new Date(data.generatedAt).toLocaleString() : 'never'} · Source: ${htmlEsc(data.account || 'steve-personal')} Drive · Originals stay in Drive
</div>
</body></html>`;
}
function renderTheme(slug, captureMode = false) {
const t = themes[slug];
if (!t) return null;
const data = loadFiles();
// Capture mode: only first 60 images to keep page light + Figma capture-friendly
if (captureMode) data.files = data.files.slice(0, 60);
const p = t.palette;
const cards = data.files.map(f => `
<div class="card" data-name="${htmlEsc((f.name||'').toLowerCase())}">
<a href="${htmlEsc(f.webViewLink)}" target="_blank" rel="noopener">
<div class="thumb">${f.thumbnailLink ? `<img loading="lazy" src="${htmlEsc(f.thumbnailLink)}" alt="${htmlEsc(f.name||'')}">` : '—'}</div>
<div class="name">${htmlEsc(f.name||'(untitled)')}</div>
<div class="folder">${htmlEsc(f.parentName||'')}</div>
</a>
</div>
`).join('');
return `<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>SDCC — ${htmlEsc(t.name)}</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root { color-scheme: ${slug === 'arctic-frost' || slug === 'botanical-garden' ? 'light' : 'dark'}; }
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; background: ${p.bg}; color: ${p.text}; font-family: ${t.fontBody}; min-height: 100vh; }
header { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; padding-bottom: 14px; margin-bottom: 18px; border-bottom: 2px solid ${p.accent}; }
h1 { margin: 0; font-family: ${t.fontHead}; font-weight: ${t.weightHead}; font-size: 30px; color: ${p.accent}; letter-spacing: -0.01em; }
.subtitle { color: ${p.accent2}; font-size: 13px; opacity: 0.9; }
.toolbar { display: flex; gap: 10px; align-items: center; }
a.back { color: ${p.accent}; text-decoration: none; font-size: 13px; padding: 6px 12px; border: 1px solid ${p.accent}; border-radius: 999px; }
a.back:hover { background: ${p.accent}; color: ${p.bg}; }
input[type=search] { background: rgba(0,0,0,.18); border: 1px solid ${p.accent2}; color: ${p.text}; padding: 8px 12px; border-radius: 8px; min-width: 240px; font-family: inherit; }
input[type=search]::placeholder { color: ${p.accent2}; opacity: 0.7; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.card { background: rgba(255,255,255,.05); border: 1px solid ${p.accent2}30; border-radius: 10px; overflow: hidden; transition: transform .12s, border-color .12s, box-shadow .15s; }
.card:hover { transform: translateY(-2px); border-color: ${p.accent}; box-shadow: 0 8px 22px rgba(0,0,0,.35); }
.card a { display: block; color: inherit; text-decoration: none; }
.thumb { aspect-ratio: 1 / 1; background: rgba(0,0,0,.18); display: flex; align-items: center; justify-content: center; }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.name { padding: 8px 10px 2px; font-size: 12px; font-family: ${t.fontHead}; font-weight: ${t.weightHead}; word-break: break-word; line-height: 1.25; color: ${p.text}; }
.folder { padding: 0 10px 8px; font-size: 10px; color: ${p.accent2}; opacity: 0.85; word-break: break-word; }
.empty { padding: 60px 20px; text-align: center; color: ${p.accent2}; border: 1px dashed ${p.accent2}66; border-radius: 12px; }
</style></head>
<body>
<header>
<div>
<h1>SDCC · ${htmlEsc(t.name)}</h1>
<div class="subtitle">${htmlEsc(t.desc)} · ${data.files.length} images</div>
</div>
<div class="toolbar">
<input id="filter" type="search" placeholder="Filter…">
<a class="back" href="/">← All themes</a>
</div>
</header>
${data.files.length === 0 ? `<div class="empty">No images indexed. Run <code>node scripts/refresh.js</code></div>` : `<div class="grid" id="grid">${cards}</div>`}
<script>
const f = document.getElementById('filter');
const cards = document.querySelectorAll('.card');
if (f) f.addEventListener('input', e => {
const q = e.target.value.trim().toLowerCase();
cards.forEach(c => c.style.display = !q || c.dataset.name.includes(q) ? '' : 'none');
});
</script>
${captureMode ? '<script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script>' : ''}
</body></html>`;
}
app.get('/', (_req, res) => res.set('Content-Type','text/html').send(renderIndex()));
app.get('/theme/:slug', (req, res) => {
const html = renderTheme(req.params.slug);
if (!html) return res.status(404).send('Unknown theme');
res.set('Content-Type','text/html').send(html);
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`SDCC Images on http://localhost:${PORT}`);
console.log(`Login: admin / DWSecure2024!`);
console.log(`Themes: ${Object.keys(themes).join(', ')}`);
});