ZDDC/landing/js/landing.js
ZDDC ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00

215 lines
8.8 KiB
JavaScript

(function() {
'use strict';
var accessibleProjects = []; // [{name, url}, ...] from server
var presets = []; // [{name, projects[]}, ...] from localStorage
var PRESETS_KEY = 'zddc_landing_presets';
// ── Initialise ──────────────────────────────────────────────────────────
async function init() {
loadPresets();
var urlProjects = getUrlProjects();
var projectList = document.getElementById('projectList');
projectList.innerHTML = '<div class="project-list-loading">Loading projects…<\/div>';
try {
var resp = await fetch(location.origin + location.pathname.replace(/\/[^\/]*$/, '/'), {
headers: { 'Accept': 'application/json' }
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
accessibleProjects = await resp.json();
} catch (e) {
projectList.innerHTML = '<div class="project-list-empty">Could not load project list: ' + escapeHtml(e.message) + '<\/div>';
return;
}
// Warn about URL projects that are not accessible
if (urlProjects.size > 0) {
var missing = Array.from(urlProjects).filter(function(p) {
return !accessibleProjects.some(function(ap) { return ap.name === p; });
});
if (missing.length > 0) {
showWarning('This link includes projects you don\'t have access to: ' + missing.map(escapeHtml).join(', '));
}
}
renderProjects(urlProjects);
renderPresetMenu();
// Close preset menu on outside click
document.addEventListener('click', function(e) {
var menu = document.getElementById('presetMenu');
var btn = document.getElementById('presetMenuBtn');
if (menu && !menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) {
menu.classList.add('hidden');
}
});
}
function getUrlProjects() {
var params = new URLSearchParams(location.search);
var val = params.get('projects');
if (!val) return new Set();
return new Set(val.split(',').map(function(p) { return p.trim(); }).filter(Boolean));
}
// ── Rendering ────────────────────────────────────────────────────────────
function renderProjects(preCheck) {
var container = document.getElementById('projectList');
if (accessibleProjects.length === 0) {
container.innerHTML = '<div class="project-list-empty">No projects available.<\/div>';
return;
}
var html = accessibleProjects.map(function(p) {
var checked = (preCheck.size === 0 || preCheck.has(p.name)) ? ' checked' : '';
return '<div class="project-item" onclick="LandingApp.toggleProject(this)">' +
'<input type="checkbox" value="' + escapeHtml(p.name) + '"' + checked + ' onclick="event.stopPropagation()">' +
'<span class="project-item-name">' + escapeHtml(p.name) + '<\/span>' +
'<\/div>';
}).join('');
container.innerHTML = html;
}
function renderPresetMenu() {
var menu = document.getElementById('presetMenu');
if (!menu) return;
if (presets.length === 0) {
menu.innerHTML = '<div class="preset-menu-empty">No presets saved.<\/div>';
return;
}
menu.innerHTML = presets.map(function(preset) {
return '<div class="preset-menu-item">' +
'<span class="preset-menu-item-name" onclick="LandingApp.applyPreset(' + JSON.stringify(preset.name) + ')">' +
escapeHtml(preset.name) + '<\/span>' +
'<button class="preset-delete-btn" onclick="LandingApp.deletePreset(' + JSON.stringify(preset.name) + ')" title="Delete preset">&times;<\/button>' +
'<\/div>';
}).join('');
}
// ── Actions ──────────────────────────────────────────────────────────────
function selectAll() {
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
cb.checked = true;
});
}
function selectNone() {
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
cb.checked = false;
});
}
function toggleProject(row) {
var cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.checked = !cb.checked;
}
function openArchive() {
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
.map(function(cb) { return cb.value; });
if (checked.length === 0) {
alert('Select at least one project to open.');
return;
}
var base = location.pathname.replace(/\/[^\/]*$/, '/');
location.href = base + 'archive.html?projects=' + checked.map(encodeURIComponent).join(',');
}
function savePreset() {
var name = prompt('Enter a name for this preset:');
if (!name || !name.trim()) return;
name = name.trim();
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
.map(function(cb) { return cb.value; });
// Replace existing preset with same name
presets = presets.filter(function(p) { return p.name !== name; });
presets.push({ name: name, projects: checked });
savePresets();
renderPresetMenu();
}
function togglePresetMenu() {
var menu = document.getElementById('presetMenu');
if (menu) menu.classList.toggle('hidden');
}
function applyPreset(name) {
var preset = presets.find(function(p) { return p.name === name; });
if (!preset) return;
var projectSet = new Set(preset.projects);
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
cb.checked = projectSet.has(cb.value);
});
document.getElementById('presetMenu').classList.add('hidden');
}
function deletePreset(name) {
presets = presets.filter(function(p) { return p.name !== name; });
savePresets();
renderPresetMenu();
}
function dismissWarning() {
var el = document.getElementById('accessWarningBanner');
if (el) el.classList.add('hidden');
}
// ── Warning ───────────────────────────────────────────────────────────────
function showWarning(message) {
var el = document.getElementById('accessWarningBanner');
var txt = document.getElementById('accessWarningText');
if (!el || !txt) return;
txt.textContent = message;
el.classList.remove('hidden');
}
// ── Persistence ───────────────────────────────────────────────────────────
function loadPresets() {
try {
var raw = localStorage.getItem(PRESETS_KEY);
presets = raw ? JSON.parse(raw) : [];
if (!Array.isArray(presets)) presets = [];
} catch (e) {
presets = [];
}
}
function savePresets() {
try {
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
} catch (e) { /* quota exceeded or private browsing */ }
}
// ── Utilities ─────────────────────────────────────────────────────────────
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// ── Bootstrap ─────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', init);
window.LandingApp = {
init: init,
selectAll: selectAll,
selectNone: selectNone,
toggleProject: toggleProject,
openArchive: openArchive,
savePreset: savePreset,
togglePresetMenu: togglePresetMenu,
applyPreset: applyPreset,
deletePreset: deletePreset,
dismissWarning: dismissWarning
};
})();