Skip to main content
Glama
gallery.js7.92 kB
/** * Gallery Client-Side Logic * Handles diagram loading, search, and interaction */ // Get port from script tag data attribute const currentScript = document.currentScript || document.querySelector("script[data-port]"); const SERVER_PORT = currentScript ? currentScript.getAttribute("data-port") : "3737"; // State let allDiagrams = []; let filteredDiagrams = []; // DOM Elements const galleryEl = document.getElementById("gallery"); const emptyStateEl = document.getElementById("emptyState"); const noResultsEl = document.getElementById("noResults"); const searchInput = document.getElementById("searchInput"); const diagramCountEl = document.getElementById("diagramCount"); // Messages const MESSAGES = { DELETE_CONFIRM: (id) => `Are you sure you want to delete diagram "${id}"?`, DELETE_FAILED: (error) => `Failed to delete diagram: ${error}`, }; /** * Formats a date as relative time (e.g., "2 hours ago") */ function formatRelativeTime(date) { const now = new Date(); const diffMs = now - new Date(date); const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return `${diffMin} min ago`; if (diffHour < 24) return `${diffHour} hour${diffHour > 1 ? "s" : ""} ago`; if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`; return new Date(date).toLocaleDateString(); } /** * Formats file size in human-readable format */ function formatFileSize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * Creates a diagram card element */ function createDiagramCard(diagram) { const card = document.createElement("div"); card.className = "diagram-card"; card.dataset.diagramId = diagram.id; // Action buttons const actions = document.createElement("div"); actions.className = "diagram-actions"; const deleteBtn = document.createElement("button"); deleteBtn.className = "diagram-action-btn delete-btn"; deleteBtn.innerHTML = "🗑️"; deleteBtn.title = "Delete"; deleteBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); deleteDiagram(diagram.id); }; actions.appendChild(deleteBtn); // Card link wrapper for preview and info const cardLink = document.createElement("a"); cardLink.href = `/view/${diagram.id}`; cardLink.className = "diagram-card-link"; // Preview section const preview = document.createElement("div"); preview.className = "diagram-preview"; if (diagram.format === "svg") { // Load SVG preview from the file system via fetch fetch(`/view/${diagram.id}`) .then((response) => response.text()) .then((html) => { // Extract SVG from the HTML response const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const svg = doc.querySelector("svg"); if (svg) { // Clone and append the SVG preview.innerHTML = ""; preview.appendChild(svg.cloneNode(true)); } else { preview.innerHTML = '<div class="diagram-preview-placeholder">📊</div>'; } }) .catch(() => { preview.innerHTML = '<div class="diagram-preview-placeholder">📊</div>'; }); } else { // For PNG/PDF, show placeholder preview.innerHTML = '<div class="diagram-preview-placeholder">📊</div>'; } // Info section const info = document.createElement("div"); info.className = "diagram-info"; const id = document.createElement("h3"); id.className = "diagram-id"; id.textContent = diagram.id; const meta = document.createElement("div"); meta.className = "diagram-meta"; const format = document.createElement("span"); format.className = "diagram-meta-item"; format.innerHTML = `<span class="diagram-format">${diagram.format}</span>`; const modified = document.createElement("span"); modified.className = "diagram-meta-item"; modified.innerHTML = `📅 ${formatRelativeTime(diagram.modifiedAt)}`; const size = document.createElement("span"); size.className = "diagram-meta-item"; size.innerHTML = `💾 ${formatFileSize(diagram.sizeBytes)}`; meta.appendChild(format); meta.appendChild(modified); meta.appendChild(size); info.appendChild(id); info.appendChild(meta); cardLink.appendChild(preview); cardLink.appendChild(info); card.appendChild(actions); card.appendChild(cardLink); return card; } /** * Renders the gallery with current filtered diagrams */ function renderGallery() { // Clear gallery galleryEl.innerHTML = ""; // Update count diagramCountEl.textContent = `${filteredDiagrams.length} diagram${filteredDiagrams.length !== 1 ? "s" : ""}`; // Show appropriate state if (allDiagrams.length === 0) { // No diagrams at all galleryEl.style.display = "none"; emptyStateEl.style.display = "block"; noResultsEl.style.display = "none"; } else if (filteredDiagrams.length === 0) { // No results from search galleryEl.style.display = "none"; emptyStateEl.style.display = "none"; noResultsEl.style.display = "block"; } else { // Show diagrams galleryEl.style.display = "grid"; emptyStateEl.style.display = "none"; noResultsEl.style.display = "none"; filteredDiagrams.forEach((diagram) => { const card = createDiagramCard(diagram); galleryEl.appendChild(card); }); } } /** * Filters diagrams based on search query */ function filterDiagrams(query) { if (!query || !query.trim()) { filteredDiagrams = [...allDiagrams]; } else { const normalizedQuery = query.toLowerCase().trim(); filteredDiagrams = allDiagrams.filter((diagram) => diagram.id.toLowerCase().includes(normalizedQuery) ); } renderGallery(); } /** * Loads diagrams from the API */ async function loadDiagrams() { try { const response = await fetch(`http://localhost:${SERVER_PORT}/api/diagrams`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); allDiagrams = data.diagrams || []; filteredDiagrams = [...allDiagrams]; renderGallery(); } catch (error) { console.error("Failed to load diagrams:", error); galleryEl.innerHTML = ` <div class="empty-state"> <div class="empty-state-icon">⚠️</div> <h2 class="empty-state-title">Failed to load diagrams</h2> <p class="empty-state-description">${error.message}</p> </div> `; } } /** * Deletes a diagram */ async function deleteDiagram(diagramId) { if (!confirm(MESSAGES.DELETE_CONFIRM(diagramId))) { return; } try { const response = await fetch(`http://localhost:${SERVER_PORT}/api/diagrams/${diagramId}`, { method: "DELETE", }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Remove from the list const index = allDiagrams.findIndex((d) => d.id === diagramId); if (index !== -1) { allDiagrams.splice(index, 1); } // Re-filter and render filterDiagrams(searchInput.value); } catch (error) { console.error("Failed to delete diagram:", error); alert(MESSAGES.DELETE_FAILED(error.message)); } } /** * Debounce function for search input */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Event Listeners searchInput.addEventListener( "input", debounce((e) => { filterDiagrams(e.target.value); }, 300) ); // Initialize loadDiagrams();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/veelenga/claude-mermaid'

If you have feedback or need assistance with the MCP directory API, please join our Discord server