app.js

  1import {
  2	initGlassTerminal,
  3	renderTerminalLayout,
  4} from "./js/components/glass-terminal.js";
  5import { initLensEffect } from "./js/components/lens.js";
  6import { initFrameworkViz } from "./js/components/framework-viz.js";
  7import { initScrollReveal } from "./js/utils/reveal.js";
  8import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js";
  9import { initSectionNav } from "./js/components/section-nav.js";
 10import { initFoundationGrid } from "./js/components/foundation-grid.js";
 11
 12// ============================================
 13// STATE
 14// ============================================
 15
 16let allCommands = [];
 17
 18// ============================================
 19// CONTENT LOADING
 20// ============================================
 21
 22function escapeHtml(value) {
 23	if (typeof value !== "string") return "";
 24	return value
 25		.replaceAll("&", "&")
 26		.replaceAll("<", "&lt;")
 27		.replaceAll(">", "&gt;")
 28		.replaceAll('"', "&quot;")
 29		.replaceAll("'", "&#39;");
 30}
 31
 32async function loadContent() {
 33	try {
 34		const [commandsRes, patternsRes] = await Promise.all([
 35			fetch("/api/commands"),
 36			fetch("/api/patterns"),
 37		]);
 38
 39		// Check for HTTP errors
 40		if (!commandsRes.ok) {
 41			throw new Error(`Commands API failed: ${commandsRes.status}`);
 42		}
 43		if (!patternsRes.ok) {
 44			throw new Error(`Patterns API failed: ${patternsRes.status}`);
 45		}
 46
 47		allCommands = await commandsRes.json();
 48		const patternsData = await patternsRes.json();
 49
 50		// Render commands (Glass Terminal)
 51		renderTerminalLayout(allCommands);
 52
 53		// Initialize gallery card stack
 54		initGalleryStack();
 55
 56		// Render patterns with tabbed navigation
 57		renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
 58	} catch (error) {
 59		console.error("Failed to load content:", error);
 60		showLoadError(error);
 61	}
 62}
 63
 64function showLoadError(error) {
 65	// Show error in commands section
 66	const commandsGallery = document.querySelector('.commands-gallery');
 67	if (commandsGallery) {
 68		commandsGallery.innerHTML = `
 69			<div class="load-error" role="alert">
 70				<div class="load-error-icon" aria-hidden="true">⚠</div>
 71				<h3 class="load-error-title">Failed to load commands</h3>
 72				<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
 73				<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
 74					Retry
 75				</button>
 76			</div>
 77		`;
 78	}
 79
 80	// Show error in patterns section
 81	const patternsContainer = document.getElementById("patterns-categories");
 82	if (patternsContainer) {
 83		patternsContainer.innerHTML = `
 84			<div class="load-error" role="alert">
 85				<div class="load-error-icon" aria-hidden="true">⚠</div>
 86				<h3 class="load-error-title">Failed to load patterns</h3>
 87				<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
 88				<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
 89					Retry
 90				</button>
 91			</div>
 92		`;
 93	}
 94}
 95
 96function initGalleryStack() {
 97	const container = document.querySelector('.gallery-stack-container');
 98	const stack = document.getElementById('gallery-stack');
 99	if (!stack || !container) return;
100
101	const cards = stack.querySelectorAll('.gallery-stack-card');
102	const counter = container.querySelector('.gallery-stack-counter');
103	const total = cards.length;
104	let current = 0;
105	let lastScroll = 0;
106
107	function update() {
108		cards.forEach((card, i) => {
109			const offset = (i - current + total) % total;
110			card.dataset.offset = offset;
111		});
112	}
113
114	function next() { current = (current + 1) % total; update(); }
115	function prev() { current = (current - 1 + total) % total; update(); }
116
117	container.querySelector('.gallery-stack-prev').addEventListener('click', prev);
118	container.querySelector('.gallery-stack-next').addEventListener('click', next);
119
120	stack.addEventListener('wheel', (e) => {
121		e.preventDefault();
122		const now = Date.now();
123		if (now - lastScroll < 350) return;
124		lastScroll = now;
125		if (e.deltaY > 0) next(); else prev();
126	}, { passive: false });
127
128	update();
129}
130
131function renderPatternsWithTabs(patterns, antipatterns) {
132	const container = document.getElementById("patterns-categories");
133	if (!container || !patterns || !antipatterns) return;
134
135	const antipatternMap = {};
136	antipatterns.forEach(cat => { antipatternMap[cat.name] = cat.items; });
137
138	const tabsHTML = patterns.map((cat, i) =>
139		`<button class="patterns-tab${i === 0 ? ' is-active' : ''}" data-index="${i}">${escapeHtml(cat.name)}</button>`
140	).join('');
141
142	const panelsHTML = patterns.map((cat, i) => {
143		const antiItems = antipatternMap[cat.name] || [];
144		return `
145		<div class="patterns-content${i === 0 ? ' is-active' : ''}" data-index="${i}">
146			<div class="patterns-col patterns-col--dont">
147				<ul>${antiItems.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
148			</div>
149			<div class="patterns-col patterns-col--do">
150				<ul>${cat.items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
151			</div>
152		</div>`;
153	}).join('');
154
155	container.innerHTML = `<div class="patterns-tabs">${tabsHTML}</div>${panelsHTML}`;
156
157	container.addEventListener('click', (e) => {
158		const tab = e.target.closest('.patterns-tab');
159		if (!tab) return;
160		const index = tab.dataset.index;
161		container.querySelectorAll('.patterns-tab').forEach(t => t.classList.remove('is-active'));
162		container.querySelectorAll('.patterns-content').forEach(p => p.classList.remove('is-active'));
163		tab.classList.add('is-active');
164		container.querySelector(`.patterns-content[data-index="${index}"]`).classList.add('is-active');
165	});
166}
167
168// ============================================
169// EVENT HANDLERS
170// ============================================
171
172// Handle bundle download clicks via event delegation.
173// Each download button carries the full bundle name in data-bundle (e.g.
174// "universal" or "universal-prefixed") so the handler is just a redirect.
175document.addEventListener("click", (e) => {
176	const bundleBtn = e.target.closest("[data-bundle]");
177	if (bundleBtn) {
178		const bundleName = bundleBtn.dataset.bundle;
179		window.location.href = `/api/download/bundle/${bundleName}`;
180	}
181
182	// Handle copy button clicks
183	const copyBtn = e.target.closest("[data-copy]");
184	if (copyBtn) {
185		const textToCopy = copyBtn.dataset.copy;
186		const onCopied = () => {
187			copyBtn.classList.add('copied');
188			setTimeout(() => copyBtn.classList.remove('copied'), 1500);
189		};
190		if (navigator.clipboard?.writeText) {
191			navigator.clipboard.writeText(textToCopy).then(onCopied).catch(() => {});
192		} else {
193			// Fallback for non-HTTPS or older browsers
194			const ta = Object.assign(document.createElement('textarea'), { value: textToCopy, style: 'position:fixed;left:-9999px' });
195			document.body.appendChild(ta);
196			ta.select();
197			try { document.execCommand('copy'); onCopied(); } catch {}
198			ta.remove();
199		}
200	}
201});
202
203
204// ============================================
205// STARTUP
206// ============================================
207
208function init() {
209	initAnchorScroll();
210	initHashTracking();
211	initLensEffect();
212	initScrollReveal();
213	initGlassTerminal();
214	initFrameworkViz();
215	initFoundationGrid();
216	initSectionNav();
217	loadContent();
218
219	document.body.classList.add("loaded");
220}
221
222if (document.readyState === "loading") {
223	document.addEventListener("DOMContentLoaded", init);
224} else {
225	init();
226}