// 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');
}
});