mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
313 lines
9.2 KiB
JavaScript
313 lines
9.2 KiB
JavaScript
import {
|
|
initGlassTerminal,
|
|
renderTerminalLayout,
|
|
} from "./js/components/glass-terminal.js";
|
|
import { initLensEffect } from "./js/components/lens.js";
|
|
import { initFrameworkViz } from "./js/components/framework-viz.js";
|
|
import { initScrollReveal } from "./js/utils/reveal.js";
|
|
import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js";
|
|
|
|
// ============================================
|
|
// STATE
|
|
// ============================================
|
|
|
|
let allCommands = [];
|
|
|
|
// ============================================
|
|
// CONTENT LOADING
|
|
// ============================================
|
|
|
|
function escapeHtml(value) {
|
|
if (typeof value !== "string") return "";
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function copyTextFallback(text) {
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
textArea.setAttribute("readonly", "");
|
|
textArea.style.position = "absolute";
|
|
textArea.style.left = "-9999px";
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
try {
|
|
return document.execCommand("copy");
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
document.body.removeChild(textArea);
|
|
}
|
|
}
|
|
|
|
async function loadContent() {
|
|
try {
|
|
const [commandsRes, patternsRes] = await Promise.all([
|
|
fetch("/api/commands"),
|
|
fetch("/api/patterns"),
|
|
]);
|
|
|
|
// Check for HTTP errors
|
|
if (!commandsRes.ok) {
|
|
throw new Error(`Commands API failed: ${commandsRes.status}`);
|
|
}
|
|
if (!patternsRes.ok) {
|
|
throw new Error(`Patterns API failed: ${patternsRes.status}`);
|
|
}
|
|
|
|
allCommands = await commandsRes.json();
|
|
const patternsData = await patternsRes.json();
|
|
|
|
// Render commands (Glass Terminal)
|
|
renderTerminalLayout(allCommands);
|
|
|
|
// Render patterns with tabbed navigation
|
|
renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
|
|
} catch (error) {
|
|
console.error("Failed to load content:", error);
|
|
showLoadError(error);
|
|
}
|
|
}
|
|
|
|
function showLoadError(error) {
|
|
// Show error in commands section
|
|
const commandsGallery = document.querySelector(".commands-gallery");
|
|
if (commandsGallery) {
|
|
commandsGallery.innerHTML = `
|
|
<div class="load-error" role="alert">
|
|
<div class="load-error-icon" aria-hidden="true">⚠</div>
|
|
<h3 class="load-error-title">Failed to load commands</h3>
|
|
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
|
|
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show error in patterns section
|
|
const patternsContainer = document.getElementById("patterns-categories");
|
|
if (patternsContainer) {
|
|
patternsContainer.innerHTML = `
|
|
<div class="load-error" role="alert">
|
|
<div class="load-error-icon" aria-hidden="true">⚠</div>
|
|
<h3 class="load-error-title">Failed to load patterns</h3>
|
|
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
|
|
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderPatternsWithTabs(patterns, antipatterns) {
|
|
const container = document.getElementById("patterns-categories");
|
|
if (!container || !patterns || !antipatterns) return;
|
|
|
|
// Create a map of antipatterns by category name
|
|
const antipatternMap = {};
|
|
antipatterns.forEach((cat) => {
|
|
antipatternMap[cat.name] = cat.items;
|
|
});
|
|
|
|
// Generate unique IDs for tabs
|
|
const tabId = (name) =>
|
|
`pattern-tab-${name.toLowerCase().replace(/\s+/g, "-")}`;
|
|
const panelId = (name) =>
|
|
`pattern-panel-${name.toLowerCase().replace(/\s+/g, "-")}`;
|
|
|
|
// Build tabs with WAI-ARIA attributes
|
|
const tabsHTML = patterns
|
|
.map((category, i) => {
|
|
const categoryName = String(category.name ?? "");
|
|
return `<button
|
|
class="pattern-tab${i === 0 ? " active" : ""}"
|
|
data-tab="${escapeHtml(categoryName)}"
|
|
role="tab"
|
|
id="${tabId(categoryName)}"
|
|
aria-selected="${i === 0 ? "true" : "false"}"
|
|
aria-controls="${panelId(categoryName)}"
|
|
tabindex="${i === 0 ? "0" : "-1"}"
|
|
>${escapeHtml(categoryName)}</button>`;
|
|
})
|
|
.join("");
|
|
|
|
// Build panels with WAI-ARIA attributes
|
|
const panelsHTML = patterns
|
|
.map((category, i) => {
|
|
const categoryName = String(category.name ?? "");
|
|
const antiItems = antipatternMap[category.name] || [];
|
|
return `
|
|
<div
|
|
class="pattern-panel${i === 0 ? " active" : ""}"
|
|
data-panel="${escapeHtml(categoryName)}"
|
|
role="tabpanel"
|
|
id="${panelId(categoryName)}"
|
|
aria-labelledby="${tabId(categoryName)}"
|
|
${i !== 0 ? "hidden" : ""}
|
|
>
|
|
<div class="pattern-columns">
|
|
<div class="pattern-column pattern-column--anti">
|
|
<span class="pattern-column-label" id="dont-label-${i}">Don't</span>
|
|
<ul class="pattern-list" aria-labelledby="dont-label-${i}">
|
|
${antiItems.map((item) => `<li class="pattern-item pattern-item--anti">${escapeHtml(item)}</li>`).join("")}
|
|
</ul>
|
|
</div>
|
|
<div class="pattern-column pattern-column--do">
|
|
<span class="pattern-column-label" id="do-label-${i}">Do</span>
|
|
<ul class="pattern-list" aria-labelledby="do-label-${i}">
|
|
${category.items.map((item) => `<li class="pattern-item pattern-item--do">${escapeHtml(item)}</li>`).join("")}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
})
|
|
.join("");
|
|
|
|
container.innerHTML = `
|
|
<div class="pattern-tabs" role="tablist" aria-label="Pattern categories">${tabsHTML}</div>
|
|
<div class="pattern-panels">${panelsHTML}</div>
|
|
`;
|
|
|
|
const tabs = container.querySelectorAll(".pattern-tab");
|
|
const panels = container.querySelectorAll(".pattern-panel");
|
|
|
|
// Function to switch tabs
|
|
const switchTab = (newTab) => {
|
|
const tabName = newTab.dataset.tab;
|
|
|
|
// Update ARIA attributes on all tabs
|
|
tabs.forEach((t) => {
|
|
t.classList.remove("active");
|
|
t.setAttribute("aria-selected", "false");
|
|
t.setAttribute("tabindex", "-1");
|
|
});
|
|
|
|
// Activate the new tab
|
|
newTab.classList.add("active");
|
|
newTab.setAttribute("aria-selected", "true");
|
|
newTab.setAttribute("tabindex", "0");
|
|
newTab.focus();
|
|
|
|
// Update panels
|
|
panels.forEach((p) => {
|
|
p.classList.remove("active");
|
|
p.setAttribute("hidden", "");
|
|
});
|
|
const escapedTabName =
|
|
typeof CSS !== "undefined" && CSS.escape
|
|
? CSS.escape(tabName)
|
|
: tabName.replaceAll('"', '\\"');
|
|
const activePanel = container.querySelector(
|
|
`[data-panel="${escapedTabName}"]`,
|
|
);
|
|
if (!activePanel) return;
|
|
activePanel.classList.add("active");
|
|
activePanel.removeAttribute("hidden");
|
|
};
|
|
|
|
// Tab click handling
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener("click", () => switchTab(tab));
|
|
});
|
|
|
|
// Keyboard navigation (Arrow keys, Home, End)
|
|
tabs.forEach((tab, index) => {
|
|
tab.addEventListener("keydown", (e) => {
|
|
let targetIndex = index;
|
|
|
|
switch (e.key) {
|
|
case "ArrowLeft":
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
targetIndex = index === 0 ? tabs.length - 1 : index - 1;
|
|
break;
|
|
case "ArrowRight":
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
targetIndex = index === tabs.length - 1 ? 0 : index + 1;
|
|
break;
|
|
case "Home":
|
|
e.preventDefault();
|
|
targetIndex = 0;
|
|
break;
|
|
case "End":
|
|
e.preventDefault();
|
|
targetIndex = tabs.length - 1;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
switchTab(tabs[targetIndex]);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// EVENT HANDLERS
|
|
// ============================================
|
|
|
|
// Handle bundle download clicks via event delegation
|
|
document.addEventListener("click", (e) => {
|
|
const bundleBtn = e.target.closest("[data-bundle]");
|
|
if (bundleBtn) {
|
|
const provider = bundleBtn.dataset.bundle;
|
|
const prefixToggle = document.getElementById("prefix-toggle");
|
|
const usePrefixed = prefixToggle && prefixToggle.checked;
|
|
const bundleName = usePrefixed ? `${provider}-prefixed` : provider;
|
|
window.location.href = `/api/download/bundle/${bundleName}`;
|
|
}
|
|
|
|
// Handle copy button clicks
|
|
const copyBtn = e.target.closest("[data-copy]");
|
|
if (copyBtn) {
|
|
const textToCopy = copyBtn.dataset.copy;
|
|
const onCopySuccess = () => {
|
|
copyBtn.classList.add("copied");
|
|
setTimeout(() => copyBtn.classList.remove("copied"), 1500);
|
|
};
|
|
|
|
if (navigator.clipboard?.writeText) {
|
|
navigator.clipboard
|
|
.writeText(textToCopy)
|
|
.then(onCopySuccess)
|
|
.catch(() => {
|
|
if (copyTextFallback(textToCopy)) {
|
|
onCopySuccess();
|
|
}
|
|
});
|
|
} else if (copyTextFallback(textToCopy)) {
|
|
onCopySuccess();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// STARTUP
|
|
// ============================================
|
|
|
|
function init() {
|
|
initAnchorScroll();
|
|
initHashTracking();
|
|
initLensEffect();
|
|
initScrollReveal();
|
|
initGlassTerminal();
|
|
initFrameworkViz();
|
|
loadContent();
|
|
|
|
document.body.classList.add("loaded");
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|