// Shopping Cart Manager for Claude Code Templates class CartManager { constructor() { this.cart = { agents: [], commands: [], settings: [], hooks: [], mcps: [], skills: [], templates: [] // Add templates support }; this.init(); } init() { // Load cart from localStorage if exists this.loadCartFromStorage(); // Clean corrupted data this.cleanCorruptedData(); this.updateCartUI(); this.updateFloatingButton(); // Use requestAnimationFrame instead of setTimeout for better performance this.scheduleButtonUpdate(); // Listen for filter changes to update buttons this.setupFilterListeners(); } // Optimized button update scheduling scheduleButtonUpdate() { if (this.updatePending) return; this.updatePending = true; requestAnimationFrame(() => { this.updateAddToCartButtons(); this.updatePending = false; }); } // Add item to cart addToCart(item, type) { // Ensure the cart type exists if (!this.cart[type]) { console.warn(`Cart type '${type}' not found. Available types:`, Object.keys(this.cart)); return false; } // Check if item already exists const existingItem = this.cart[type].find(cartItem => cartItem.path === item.path); if (existingItem) { this.showNotification(`${item.name} is already in your stack!`, 'warning'); return false; } // Add item to cart this.cart[type].push({ name: item.name, path: item.path, category: item.category || '', description: item.description || '', icon: this.getTypeIcon(type) }); this.saveCartToStorage(); this.updateCartUI(); this.updateFloatingButton(); this.showNotification(`${item.name} added to stack!`, 'success'); // Track cart add event window.eventTracker?.track('cart_add', { component_type: type, component_name: item.name, cart_size: this.getTotalItems() }); return true; } // Remove item from cart removeFromCart(itemPath, type) { // Ensure the cart type exists if (!this.cart[type]) { console.warn(`Cart type '${type}' not found. Available types:`, Object.keys(this.cart)); return; } const removedItem = this.cart[type].find(item => item.path === itemPath); this.cart[type] = this.cart[type].filter(item => item.path !== itemPath); this.saveCartToStorage(); this.updateCartUI(); this.updateFloatingButton(); this.showNotification('Item removed from stack', 'info'); // Track cart remove event window.eventTracker?.track('cart_remove', { component_type: type, component_name: removedItem?.name || itemPath, cart_size: this.getTotalItems() }); } // Clear entire cart clearCart() { // Initialize cart structure to prevent errors this.initializeCartStructure(); if (this.getTotalItems() === 0) return; if (confirm('Are you sure you want to clear your entire stack?')) { this.cart = { agents: [], commands: [], settings: [], hooks: [], mcps: [], skills: [], templates: [] }; this.saveCartToStorage(); this.updateCartUI(); this.updateFloatingButton(); this.showNotification('Stack cleared', 'info'); } } // Get total items count getTotalItems() { this.initializeCartStructure(); return (this.cart.agents?.length || 0) + (this.cart.commands?.length || 0) + (this.cart.settings?.length || 0) + (this.cart.hooks?.length || 0) + (this.cart.mcps?.length || 0) + (this.cart.skills?.length || 0) + (this.cart.templates?.length || 0); } // Ensure cart has all required arrays initializeCartStructure() { if (!this.cart.agents) this.cart.agents = []; if (!this.cart.commands) this.cart.commands = []; if (!this.cart.settings) this.cart.settings = []; if (!this.cart.hooks) this.cart.hooks = []; if (!this.cart.mcps) this.cart.mcps = []; if (!this.cart.skills) this.cart.skills = []; if (!this.cart.templates) this.cart.templates = []; } // Clean corrupted data from cart cleanCorruptedData() { let cleaned = false; Object.keys(this.cart).forEach(type => { if (Array.isArray(this.cart[type])) { const originalLength = this.cart[type].length; this.cart[type] = this.cart[type].filter(item => item && typeof item === 'object' && item.name && item.path ); if (this.cart[type].length !== originalLength) { cleaned = true; } } }); if (cleaned) { this.saveCartToStorage(); } } // Clean path by removing .md and .json extensions getCleanPath(path) { if (!path) return path; // Remove .md extension if present if (path.endsWith('.md')) { return path.slice(0, -3); } // Remove .json extension if present if (path.endsWith('.json')) { return path.slice(0, -5); } return path; } // Clean component name by removing extensions and formatting getCleanComponentName(name) { if (!name) { return 'Unknown Component'; } let cleanName = name; // Remove .md extension if present if (cleanName.endsWith('.md')) { cleanName = cleanName.slice(0, -3); } // Remove .json extension if present if (cleanName.endsWith('.json')) { cleanName = cleanName.slice(0, -5); } // Convert kebab-case or snake_case to Title Case cleanName = cleanName .replace(/[-_]/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); return cleanName; } // Check if item is in cart isInCart(itemPath, type) { // Ensure the cart type exists if (!this.cart[type]) { console.warn(`Cart type '${type}' not found. Available types:`, Object.keys(this.cart)); return false; } return this.cart[type].some(item => item.path === itemPath); } // Update cart UI updateCartUI() { const cartEmpty = document.getElementById('cartEmpty'); const cartItems = document.getElementById('cartItems'); const cartFooter = document.getElementById('cartFooter'); const cartClearProminent = document.getElementById('cartClearProminent'); const clearCount = document.getElementById('clearCount'); // Early return if essential elements don't exist (e.g., on component page) if (!cartEmpty || !cartItems || !cartFooter) { return; } const totalItems = this.getTotalItems(); if (totalItems === 0) { cartEmpty.style.display = 'block'; cartItems.style.display = 'none'; cartFooter.style.display = 'none'; // Hide prominent clear button when cart is empty if (cartClearProminent) cartClearProminent.style.display = 'none'; } else { cartEmpty.style.display = 'none'; cartItems.style.display = 'block'; cartFooter.style.display = 'block'; // Show and update prominent clear button if (cartClearProminent) { cartClearProminent.style.display = 'block'; if (clearCount) { clearCount.textContent = `(${totalItems})`; } } this.renderCartItems(); this.updateCommand(); } } // Render cart items renderCartItems() { // Update counts - only if elements exist const agentsCount = document.getElementById('agentsCount'); const commandsCount = document.getElementById('commandsCount'); const settingsCount = document.getElementById('settingsCount'); const hooksCount = document.getElementById('hooksCount'); const mcpsCount = document.getElementById('mcpsCount'); const skillsCount = document.getElementById('skillsCount'); if (agentsCount) agentsCount.textContent = this.cart.agents.length; if (commandsCount) commandsCount.textContent = this.cart.commands.length; if (settingsCount) settingsCount.textContent = this.cart.settings.length; if (hooksCount) hooksCount.textContent = this.cart.hooks.length; if (mcpsCount) mcpsCount.textContent = this.cart.mcps.length; if (skillsCount) skillsCount.textContent = this.cart.skills.length; // Render sections this.renderSection('agents', 'agentsList'); this.renderSection('commands', 'commandsList'); this.renderSection('settings', 'settingsList'); this.renderSection('hooks', 'hooksList'); this.renderSection('mcps', 'mcpsList'); this.renderSection('skills', 'skillsList'); } // Render a specific section renderSection(type, containerId) { const container = document.getElementById(containerId); // Skip if container doesn't exist (e.g., on component page) if (!container) { return; } const items = this.cart[type]; if (items.length === 0) { container.innerHTML = '
No items added
'; return; } container.innerHTML = items.map(item => `
${item.icon}
${this.getCleanComponentName(item.name)}
${this.getCleanPath(item.path)}
`).join(''); } // Update generated command updateCommand() { let command = 'npx claude-code-templates@latest'; if (this.cart.agents.length > 0) { const agentPaths = this.cart.agents.map(item => this.getCleanPath(item.path)).join(','); command += ` --agent ${agentPaths}`; } if (this.cart.commands.length > 0) { const commandPaths = this.cart.commands.map(item => this.getCleanPath(item.path)).join(','); command += ` --command "${commandPaths}"`; } if (this.cart.settings.length > 0) { const settingsPaths = this.cart.settings.map(item => this.getCleanPath(item.path)).join(','); command += ` --setting "${settingsPaths}"`; } if (this.cart.hooks.length > 0) { const hooksPaths = this.cart.hooks.map(item => this.getCleanPath(item.path)).join(','); command += ` --hook "${hooksPaths}"`; } if (this.cart.mcps.length > 0) { const mcpPaths = this.cart.mcps.map(item => this.getCleanPath(item.path)).join(','); command += ` --mcp "${mcpPaths}"`; } if (this.cart.skills.length > 0) { const skillPaths = this.cart.skills.map(item => this.getCleanPath(item.path)).join(','); command += ` --skill "${skillPaths}"`; } const generatedCommand = document.getElementById('generatedCommand'); if (generatedCommand) { generatedCommand.textContent = command; } } // Update floating button updateFloatingButton() { const floatingBtn = document.getElementById('cartFloatingBtn'); const badge = document.getElementById('cartBadge'); // Skip if floating button doesn't exist (e.g., on component page) if (!floatingBtn || !badge) { return; } const totalItems = this.getTotalItems(); if (totalItems > 0) { floatingBtn.style.display = 'flex'; badge.textContent = totalItems; } else { floatingBtn.style.display = 'none'; } // Update all add-to-cart buttons in the page this.updateAddToCartButtons(); } // Update add-to-cart buttons state updateAddToCartButtons() { document.querySelectorAll('.add-to-cart-btn').forEach(btn => { const type = btn.dataset.type; const path = btn.dataset.path; if (this.isInCart(path, type)) { btn.classList.add('added'); btn.innerHTML = ` Added to Stack `; } else { btn.classList.remove('added'); btn.innerHTML = ` Add to Stack `; } }); } // Get type icon getTypeIcon(type) { const icons = { agents: '🤖', commands: '⚡', settings: 'âš™ī¸', hooks: 'đŸĒ', mcps: '🔌', skills: '🎨', templates: 'đŸ“Ļ' }; return icons[type] || 'đŸ“Ļ'; } // Save cart to localStorage saveCartToStorage() { localStorage.setItem('claudeCodeCart', JSON.stringify(this.cart)); } // Load cart from localStorage loadCartFromStorage() { const saved = localStorage.getItem('claudeCodeCart'); if (saved) { try { this.cart = JSON.parse(saved); // Ensure all arrays exist using centralized method this.initializeCartStructure(); if (!this.cart.templates) this.cart.templates = []; if (!this.cart.skills) this.cart.skills = []; } catch (e) { console.warn('Failed to load cart from storage:', e); this.cart = { agents: [], commands: [], settings: [], hooks: [], mcps: [], skills: [], templates: [] }; } } } // Optimized filter listeners with debouncing setupFilterListeners() { // Debounced update function let updateTimeout; const debouncedUpdate = () => { clearTimeout(updateTimeout); updateTimeout = setTimeout(() => this.scheduleButtonUpdate(), 150); }; // Listen for filter button clicks (more efficient than MutationObserver) document.addEventListener('click', (e) => { if (e.target.classList.contains('filter-btn') || e.target.closest('.filter-btn')) { debouncedUpdate(); } }); // Lightweight MutationObserver for critical changes only const observer = new MutationObserver((mutations) => { let shouldUpdate = false; for (const mutation of mutations) { // Only update for significant DOM changes if (mutation.type === 'childList' && mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && (node.classList?.contains('unified-card') || node.querySelector?.('.unified-card')) )) { shouldUpdate = true; break; } } if (shouldUpdate) { debouncedUpdate(); } }); // Observe only the unified grid with specific options const unifiedGrid = document.getElementById('unifiedGrid'); if (unifiedGrid) { observer.observe(unifiedGrid, { childList: true, subtree: false // Only direct children, not deep nesting }); } } // Show notification showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `cart-notification cart-notification-${type}`; notification.innerHTML = `
${this.getNotificationIcon(type)} ${message}
`; // Add to page document.body.appendChild(notification); // Show with animation using requestAnimationFrame requestAnimationFrame(() => { requestAnimationFrame(() => notification.classList.add('show')); }); // Remove after 3 seconds setTimeout(() => { notification.classList.remove('show'); setTimeout(() => notification.remove(), 300); }, 3000); } // Get notification icon getNotificationIcon(type) { const icons = { success: '✅', warning: 'âš ī¸', error: '❌', info: 'â„šī¸' }; return icons[type] || 'â„šī¸'; } } // Global cart manager instance const cartManager = new CartManager(); // Global functions for cart operations function openCart() { const sidebar = document.getElementById('shoppingCart'); const floatingButton = document.getElementById('cartFloatingBtn'); sidebar.classList.add('active'); // Hide floating cart button when sidebar is open if (floatingButton) { floatingButton.style.transform = 'translateX(100px)'; floatingButton.style.opacity = '0'; floatingButton.style.pointerEvents = 'none'; } // Create subtle overlay that doesn't block content visibility if (window.innerWidth > 768) { const overlay = document.createElement('div'); overlay.id = 'cartOverlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.15); z-index: 999; opacity: 0; transition: opacity 0.3s ease; pointer-events: auto; `; document.body.appendChild(overlay); // Fade in overlay setTimeout(() => overlay.style.opacity = '1', 10); // Close on overlay click overlay.addEventListener('click', closeCart); } } function closeCart() { const sidebar = document.getElementById('shoppingCart'); const floatingButton = document.getElementById('cartFloatingBtn'); sidebar.classList.remove('active'); // Show floating cart button again when sidebar is closed if (floatingButton) { floatingButton.style.transform = 'translateX(0)'; floatingButton.style.opacity = '1'; floatingButton.style.pointerEvents = 'auto'; } // Remove dynamic overlay const overlay = document.getElementById('cartOverlay'); if (overlay) { overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 300); } } function clearCart() { cartManager.clearCart(); } function copyCartCommand() { const command = document.getElementById('generatedCommand').textContent; copyToClipboard(command, 'Command copied to clipboard!'); // Track cart checkout (copy command) const cart = cartManager.cart; const types = Object.keys(cart).filter(k => cart[k].length > 0); const items = types.reduce((sum, k) => sum + cart[k].length, 0); window.eventTracker?.track('cart_checkout', { items, types }); } function downloadStack() { const command = document.getElementById('generatedCommand').textContent; // Ask user if they want to execute the command const shouldExecute = confirm(`Ready to download your stack?\n\nThis will run:\n${command}\n\nDo you want to proceed?`); if (shouldExecute) { // Copy command to clipboard for user convenience copyToClipboard(command); // Show instructions cartManager.showNotification('Command copied! Paste it in your terminal to download the stack.', 'success'); // Optionally clear the cart after download setTimeout(() => { if (confirm('Stack command is ready! Would you like to clear your cart?')) { cartManager.clearCart(); closeCart(); } }, 2000); } } function addToCart(item, type) { return cartManager.addToCart(item, type); } // Close cart when clicking on overlay (handled by dynamic overlay now) /* document.addEventListener('click', (e) => { const sidebar = document.getElementById('shoppingCart'); if (e.target === sidebar && sidebar.classList.contains('active')) { closeCart(); } }); */ // Close cart with escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeCart(); } }); // Share functionality - Dropdown style function toggleShareDropdown() { const shareDropdown = document.getElementById('shareDropdown'); shareDropdown.classList.toggle('open'); } function shareOnTwitter() { const command = document.getElementById('generatedCommand').textContent.trim(); const message = `🚀 Just created this Claude Code Templates stack! Just run this command: ${command} Create yours at https://aitmpl.com`; const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}`; window.open(twitterUrl, '_blank'); // Close dropdown after sharing document.getElementById('shareDropdown').classList.remove('open'); } function shareOnThreads() { const command = document.getElementById('generatedCommand').textContent.trim(); const message = `🚀 Just created this Claude Code Templates stack! Just run this command: ${command} Create yours at https://aitmpl.com`; const threadsUrl = `https://threads.net/intent/post?text=${encodeURIComponent(message)}`; window.open(threadsUrl, '_blank'); // Close dropdown after sharing document.getElementById('shareDropdown').classList.remove('open'); } // Close dropdown when clicking outside document.addEventListener('click', (e) => { const shareDropdown = document.getElementById('shareDropdown'); if (shareDropdown && !shareDropdown.contains(e.target)) { shareDropdown.classList.remove('open'); } });