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("<", "<")
27 .replaceAll(">", ">")
28 .replaceAll('"', """)
29 .replaceAll("'", "'");
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}