chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
// Company Stacks Carousel functionality
class CompanyCarousel {
constructor() {
this.carousel = document.getElementById('companyCarousel');
this.leftBtn = document.querySelector('.carousel-btn-left');
this.rightBtn = document.querySelector('.carousel-btn-right');
this.scrollAmount = 280; // Width of one card plus gap
this.autoScrollInterval = null;
this.init();
}
init() {
if (!this.carousel) return;
this.updateButtons();
this.setupEventListeners();
this.startAutoScroll();
}
setupEventListeners() {
// Scroll event to update button states
this.carousel.addEventListener('scroll', () => {
this.updateButtons();
});
// Stop auto-scroll on hover
this.carousel.addEventListener('mouseenter', () => {
this.stopAutoScroll();
});
// Resume auto-scroll when mouse leaves
this.carousel.addEventListener('mouseleave', () => {
this.startAutoScroll();
});
// Touch/swipe support for mobile
let startX = 0;
let scrollLeft = 0;
let isDown = false;
this.carousel.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - this.carousel.offsetLeft;
scrollLeft = this.carousel.scrollLeft;
this.carousel.style.cursor = 'grabbing';
this.stopAutoScroll();
});
this.carousel.addEventListener('mouseleave', () => {
isDown = false;
this.carousel.style.cursor = 'grab';
});
this.carousel.addEventListener('mouseup', () => {
isDown = false;
this.carousel.style.cursor = 'grab';
this.startAutoScroll();
});
this.carousel.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - this.carousel.offsetLeft;
const walk = (x - startX) * 2;
this.carousel.scrollLeft = scrollLeft - walk;
});
// Touch events for mobile
this.carousel.addEventListener('touchstart', (e) => {
startX = e.touches[0].pageX - this.carousel.offsetLeft;
scrollLeft = this.carousel.scrollLeft;
this.stopAutoScroll();
});
this.carousel.addEventListener('touchmove', (e) => {
if (!startX) return;
const x = e.touches[0].pageX - this.carousel.offsetLeft;
const walk = (x - startX) * 2;
this.carousel.scrollLeft = scrollLeft - walk;
});
this.carousel.addEventListener('touchend', () => {
startX = 0;
this.startAutoScroll();
});
}
scrollLeft() {
this.carousel.scrollBy({
left: -this.scrollAmount,
behavior: 'smooth'
});
this.stopAutoScroll();
setTimeout(() => this.startAutoScroll(), 3000);
}
scrollRight() {
this.carousel.scrollBy({
left: this.scrollAmount,
behavior: 'smooth'
});
this.stopAutoScroll();
setTimeout(() => this.startAutoScroll(), 3000);
}
updateButtons() {
const scrollLeft = this.carousel.scrollLeft;
const maxScroll = this.carousel.scrollWidth - this.carousel.clientWidth;
// Update left button
if (this.leftBtn) {
this.leftBtn.disabled = scrollLeft <= 0;
this.leftBtn.style.opacity = scrollLeft <= 0 ? '0.5' : '1';
}
// Update right button
if (this.rightBtn) {
this.rightBtn.disabled = scrollLeft >= maxScroll - 1; // -1 for rounding errors
this.rightBtn.style.opacity = scrollLeft >= maxScroll - 1 ? '0.5' : '1';
}
}
startAutoScroll() {
this.stopAutoScroll();
this.autoScrollInterval = setInterval(() => {
const maxScroll = this.carousel.scrollWidth - this.carousel.clientWidth;
const currentScroll = this.carousel.scrollLeft;
if (currentScroll >= maxScroll - 1) {
// Reset to beginning
this.carousel.scrollTo({
left: 0,
behavior: 'smooth'
});
} else {
// Scroll to next item
this.scrollRight();
}
}, 4000); // Auto-scroll every 4 seconds
}
stopAutoScroll() {
if (this.autoScrollInterval) {
clearInterval(this.autoScrollInterval);
this.autoScrollInterval = null;
}
}
}
// Global functions for button clicks
function scrollCarousel(direction) {
if (window.companyCarousel) {
if (direction === 'left') {
window.companyCarousel.scrollLeft();
} else {
window.companyCarousel.scrollRight();
}
}
}
// Featured Projects Carousel functionality
class FeaturedCarousel {
constructor() {
this.carousel = document.getElementById('featuredCarousel');
this.leftBtn = document.querySelector('.featured-section .carousel-btn-left');
this.rightBtn = document.querySelector('.featured-section .carousel-btn-right');
this.scrollAmount = 304; // Width of one featured card (280px) plus gap (24px)
this.init();
}
init() {
if (!this.carousel) return;
this.updateButtons();
this.setupEventListeners();
}
setupEventListeners() {
// Scroll event to update button states
this.carousel.addEventListener('scroll', () => {
this.updateButtons();
});
// Touch/swipe support for mobile
let startX = 0;
let scrollLeft = 0;
let isDown = false;
this.carousel.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - this.carousel.offsetLeft;
scrollLeft = this.carousel.scrollLeft;
this.carousel.style.cursor = 'grabbing';
});
this.carousel.addEventListener('mouseleave', () => {
isDown = false;
this.carousel.style.cursor = 'grab';
});
this.carousel.addEventListener('mouseup', () => {
isDown = false;
this.carousel.style.cursor = 'grab';
});
this.carousel.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - this.carousel.offsetLeft;
const walk = (x - startX) * 2;
this.carousel.scrollLeft = scrollLeft - walk;
});
// Touch events for mobile
this.carousel.addEventListener('touchstart', (e) => {
startX = e.touches[0].pageX - this.carousel.offsetLeft;
scrollLeft = this.carousel.scrollLeft;
});
this.carousel.addEventListener('touchmove', (e) => {
if (!startX) return;
const x = e.touches[0].pageX - this.carousel.offsetLeft;
const walk = (x - startX) * 2;
this.carousel.scrollLeft = scrollLeft - walk;
});
this.carousel.addEventListener('touchend', () => {
startX = 0;
});
}
scrollLeft() {
this.carousel.scrollBy({
left: -this.scrollAmount,
behavior: 'smooth'
});
}
scrollRight() {
this.carousel.scrollBy({
left: this.scrollAmount,
behavior: 'smooth'
});
}
updateButtons() {
const scrollLeft = this.carousel.scrollLeft;
const maxScroll = this.carousel.scrollWidth - this.carousel.clientWidth;
// Update left button
if (this.leftBtn) {
this.leftBtn.disabled = scrollLeft <= 0;
this.leftBtn.style.opacity = scrollLeft <= 0 ? '0.5' : '1';
}
// Update right button
if (this.rightBtn) {
this.rightBtn.disabled = scrollLeft >= maxScroll - 1;
this.rightBtn.style.opacity = scrollLeft >= maxScroll - 1 ? '0.5' : '1';
}
}
}
// Global functions for button clicks
function scrollFeaturedCarousel(direction) {
if (window.featuredCarousel) {
if (direction === 'left') {
window.featuredCarousel.scrollLeft();
} else {
window.featuredCarousel.scrollRight();
}
}
}
// Initialize carousel when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.companyCarousel = new CompanyCarousel();
window.featuredCarousel = new FeaturedCarousel();
});
// Re-initialize if page content changes
function reinitializeCarousel() {
if (window.companyCarousel) {
window.companyCarousel.stopAutoScroll();
}
window.companyCarousel = new CompanyCarousel();
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = { CompanyCarousel, scrollCarousel, reinitializeCarousel };
}
+692
View File
@@ -0,0 +1,692 @@
// 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 = '<div class="no-items">No items added</div>';
return;
}
container.innerHTML = items.map(item => `
<div class="cart-item">
<div class="cart-item-icon">${item.icon}</div>
<div class="cart-item-info">
<div class="cart-item-name">${this.getCleanComponentName(item.name)}</div>
<div class="cart-item-path">${this.getCleanPath(item.path)}</div>
</div>
<button class="cart-item-remove" onclick="cartManager.removeFromCart('${item.path}', '${type}')" title="Remove from stack">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/>
</svg>
</button>
</div>
`).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 = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
</svg>
Added to Stack
`;
} else {
btn.classList.remove('added');
btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19,7H18V6A2,2 0 0,0 16,4H8A2,2 0 0,0 6,6V7H5A1,1 0 0,0 4,8A1,1 0 0,0 5,9H6V19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V9H19A1,1 0 0,0 20,8A1,1 0 0,0 19,7M8,6H16V7H8V6M16,19H8V9H16V19Z"/>
</svg>
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 = `
<div class="notification-content">
<span class="notification-icon">${this.getNotificationIcon(type)}</span>
<span class="notification-message">${message}</span>
</div>
`;
// 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');
}
});
File diff suppressed because it is too large Load Diff
+633
View File
@@ -0,0 +1,633 @@
// Data Loader - Handles loading component data from various sources
class DataLoader {
constructor() {
this.componentsData = null;
this.fullComponentsData = null; // Store full data for accurate counts
this.templatesData = null; // Deprecated - templates now in components.json
this.metadataData = null; // External metadata for tags, companies, technologies
this.loadingStates = {
components: false
};
this.cache = new Map();
this.TIMEOUT_MS = 8000; // 8 seconds timeout
this.ITEMS_PER_PAGE = 50; // Lazy loading batch size
}
// Get data file paths (always absolute for production)
getDataPath(filename) {
return '/' + filename;
}
// Load all components at once (simplified approach)
async loadAllComponents() {
try {
this.loadingStates.components = true;
this.showLoadingState('components', true);
const cacheKey = 'all_components';
if (this.cache.has(cacheKey)) {
this.showLoadingState('components', false);
this.loadingStates.components = false;
return this.cache.get(cacheKey);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS);
const response = await fetch(this.getDataPath('components.json'), {
signal: controller.signal,
headers: {
'Cache-Control': 'max-age=300' // 5 minutes cache
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const allData = await response.json();
// Store both full and component data
this.fullComponentsData = allData;
this.componentsData = allData;
// Load metadata
await this.loadMetadata();
this.cache.set(cacheKey, allData);
this.showLoadingState('components', false);
this.loadingStates.components = false;
return this.componentsData;
} catch (error) {
this.showLoadingState('components', false);
this.loadingStates.components = false;
if (error.name === 'AbortError') {
console.error('Components loading timed out after', this.TIMEOUT_MS + 'ms');
this.showError('Loading timed out. Using fallback data.');
} else {
console.error('Error loading components:', error);
this.showError('Failed to load components. Using fallback data.');
}
return this.getFallbackComponentData();
}
}
// Load components with lazy loading and timeout (kept for backward compatibility)
async loadComponents(page = 1, itemsPerPage = this.ITEMS_PER_PAGE) {
try {
this.loadingStates.components = true;
this.showLoadingState('components', true);
const cacheKey = `components_${page}_${itemsPerPage}`;
if (this.cache.has(cacheKey)) {
this.showLoadingState('components', false);
this.loadingStates.components = false;
return this.cache.get(cacheKey);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS);
const response = await fetch(this.getDataPath('components.json'), {
signal: controller.signal,
headers: {
'Cache-Control': 'max-age=300' // 5 minutes cache
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fullData = await response.json();
// Store full data for accurate counts (only on first load)
if (page === 1) {
this.fullComponentsData = fullData;
}
// Apply pagination to reduce memory usage
const paginatedData = this.paginateComponents(fullData, page, itemsPerPage);
// Store full data for templates if it exists
if (fullData.templates && page === 1) {
paginatedData.templates = fullData.templates; // Templates don't need pagination
}
this.cache.set(cacheKey, paginatedData);
this.componentsData = paginatedData;
this.showLoadingState('components', false);
this.loadingStates.components = false;
return this.componentsData;
} catch (error) {
this.showLoadingState('components', false);
this.loadingStates.components = false;
if (error.name === 'AbortError') {
console.error('Components loading timed out after', this.TIMEOUT_MS + 'ms');
this.showError('Loading timed out. Using fallback data.');
} else {
console.error('Error loading components:', error);
this.showError('Failed to load components. Using fallback data.');
}
return this.getFallbackComponentData();
}
}
// Get fallback component data when components.json is unavailable
getFallbackComponentData() {
return {
agents: [
{ name: 'code-reviewer', path: 'development/code-reviewer', category: 'development', type: 'agent', content: 'AI-powered code reviewer that analyzes your code for best practices, security issues, and optimization opportunities.' },
{ name: 'documentation-writer', path: 'development/documentation-writer', category: 'development', type: 'agent', content: 'Generates comprehensive documentation for your codebase, including API docs, README files, and inline comments.' },
{ name: 'bug-hunter', path: 'development/bug-hunter', category: 'development', type: 'agent', content: 'Specialized in finding and fixing bugs through systematic code analysis and testing.' },
{ name: 'security-auditor', path: 'security/security-auditor', category: 'security', type: 'agent', content: 'Performs security audits on your code to identify vulnerabilities and security best practices.' },
{ name: 'performance-optimizer', path: 'optimization/performance-optimizer', category: 'optimization', type: 'agent', content: 'Analyzes and optimizes code performance, identifying bottlenecks and suggesting improvements.' }
],
commands: [
{ name: 'git-setup', path: 'development/git-setup', category: 'development', type: 'command', content: 'Sets up Git repository with best practices, including .gitignore, hooks, and workflow configurations.' },
{ name: 'project-init', path: 'development/project-init', category: 'development', type: 'command', content: 'Initializes new projects with proper structure, dependencies, and configuration files.' },
{ name: 'docker-setup', path: 'devops/docker-setup', category: 'devops', type: 'command', content: 'Creates Docker configurations including Dockerfile, docker-compose, and container optimization.' },
{ name: 'test-runner', path: 'testing/test-runner', category: 'testing', type: 'command', content: 'Sets up comprehensive testing frameworks and runs automated test suites.' },
{ name: 'build-pipeline', path: 'devops/build-pipeline', category: 'devops', type: 'command', content: 'Configures CI/CD pipelines for automated building, testing, and deployment.' }
],
mcps: [
{ name: 'database-connector', path: 'database/database-connector', category: 'database', type: 'mcp', content: 'Connects to various databases and provides query and management capabilities.' },
{ name: 'api-client', path: 'api/api-client', category: 'api', type: 'mcp', content: 'HTTP client for interacting with REST APIs and web services.' },
{ name: 'file-manager', path: 'system/file-manager', category: 'system', type: 'mcp', content: 'File system operations including reading, writing, and organizing files.' },
{ name: 'redis-cache', path: 'database/redis-cache', category: 'database', type: 'mcp', content: 'Redis caching implementation for improved application performance.' },
{ name: 'email-service', path: 'communication/email-service', category: 'communication', type: 'mcp', content: 'Email sending and management service with template support.' }
]
};
}
// Load templates - now using components.json data (templates deprecated)
async loadTemplates() {
try {
// Templates are now included in components.json as 'templates' section
// This method is kept for backward compatibility
if (this.componentsData && this.componentsData.templates) {
this.templatesData = { templates: this.componentsData.templates };
return this.templatesData;
}
// If components aren't loaded yet, try to load them
if (!this.componentsData) {
await this.loadComponents();
if (this.componentsData && this.componentsData.templates) {
this.templatesData = { templates: this.componentsData.templates };
return this.templatesData;
}
}
// Return empty if no templates in components.json
console.warn('No templates found in components.json');
this.templatesData = {};
return this.templatesData;
} catch (error) {
console.error('Error loading templates from components:', error);
this.templatesData = {};
return this.templatesData;
}
}
// Parse the templates.js file content to extract TEMPLATES_CONFIG
parseTemplatesConfig(fileContent) {
try {
const configMatch = fileContent.match(/const TEMPLATES_CONFIG = ({[\s\S]*?});/);
if (!configMatch) {
throw new Error('TEMPLATES_CONFIG not found in file');
}
let configString = configMatch[1];
configString = configString.replace(/'/g, '"');
configString = configString.replace(/(\w+):/g, '"$1":');
configString = configString.replace(/,(\s*[}\]])/g, '$1');
const config = JSON.parse(configString);
return this.transformTemplatesData(config);
} catch (error) {
console.error('Error parsing templates config:', error);
throw error;
}
}
// Transform templates data to a unified format
transformTemplatesData(config) {
const unified = {};
Object.entries(config).forEach(([category, items]) => {
unified[category] = Object.entries(items).map(([key, template]) => ({
id: key,
name: template.name,
description: template.description || '',
type: 'template',
category: category,
folderPath: template.folderPath || '',
content: template.description || '',
files: template.files || [],
...template
}));
});
return unified;
}
// Get all components as a flat array
getAllComponents() {
if (!this.componentsData) return [];
const allComponents = [];
['agents', 'commands', 'mcps', 'skills'].forEach(type => {
if (this.componentsData[type]) {
allComponents.push(...this.componentsData[type]);
}
});
return allComponents;
}
// Find component by name and type
findComponent(name, type) {
if (!this.componentsData) return null;
const typeKey = type + 's';
if (!this.componentsData[typeKey]) return null;
return this.componentsData[typeKey].find(component => component.name === name);
}
// Get components by type
getComponentsByType(type) {
if (!this.componentsData) return [];
const typeKey = type + 's';
return this.componentsData[typeKey] || [];
}
// Paginate components data to reduce memory usage
paginateComponents(fullData, page, itemsPerPage) {
const paginatedData = {
agents: [],
commands: [],
mcps: [],
skills: [],
templates: [] // Include templates in pagination structure
};
['agents', 'commands', 'mcps', 'skills'].forEach(type => {
if (fullData[type]) {
const startIndex = (page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
paginatedData[type] = fullData[type].slice(startIndex, endIndex);
}
});
// Templates don't need pagination - include all if available
if (fullData.templates) {
paginatedData.templates = fullData.templates;
}
return paginatedData;
}
// Show loading states
showLoadingState(type, isLoading) {
const loadingElement = document.getElementById(`${type}-loading`);
const contentElement = document.getElementById(`${type}-content`);
if (loadingElement && contentElement) {
if (isLoading) {
loadingElement.style.display = 'flex';
contentElement.style.opacity = '0.5';
} else {
loadingElement.style.display = 'none';
contentElement.style.opacity = '1';
}
}
// Also update any loading spinners in the UI
const spinners = document.querySelectorAll('.loading-spinner');
spinners.forEach(spinner => {
spinner.style.display = isLoading ? 'block' : 'none';
});
}
// Show error messages
showError(message) {
// Try to use existing notification system
if (window.showNotification) {
window.showNotification(message, 'error', 5000);
} else {
console.warn(message);
// Create simple toast notification
const toast = document.createElement('div');
toast.className = 'error-toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f85149;
color: white;
padding: 12px 16px;
border-radius: 6px;
z-index: 1000;
font-size: 14px;
max-width: 300px;
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
}
}
// Load more components (no longer needed but kept for compatibility)
async loadMoreComponents(page) {
// All components are now loaded at once, so this method returns null
return null;
}
// Get the total counts from full data for accurate filter counts
getTotalCounts() {
if (this.fullComponentsData) {
return {
agents: this.fullComponentsData.agents ? this.fullComponentsData.agents.length : 0,
commands: this.fullComponentsData.commands ? this.fullComponentsData.commands.length : 0,
mcps: this.fullComponentsData.mcps ? this.fullComponentsData.mcps.length : 0,
settings: this.fullComponentsData.settings ? this.fullComponentsData.settings.length : 0,
hooks: this.fullComponentsData.hooks ? this.fullComponentsData.hooks.length : 0,
skills: this.fullComponentsData.skills ? this.fullComponentsData.skills.length : 0,
templates: this.fullComponentsData.templates ? this.fullComponentsData.templates.length : 0
};
}
// Fallback to current loaded data
return {
agents: this.componentsData?.agents?.length || 0,
commands: this.componentsData?.commands?.length || 0,
mcps: this.componentsData?.mcps?.length || 0,
settings: this.componentsData?.settings?.length || 0,
hooks: this.componentsData?.hooks?.length || 0,
skills: this.componentsData?.skills?.length || 0,
templates: this.componentsData?.templates?.length || 0
};
}
// Load external metadata
async loadMetadata() {
try {
const cacheKey = 'metadata';
if (this.cache.has(cacheKey)) {
this.metadataData = this.cache.get(cacheKey);
return this.metadataData;
}
const response = await fetch(this.getDataPath('components-metadata.json'), {
headers: {
'Cache-Control': 'max-age=300' // 5 minutes cache
}
});
if (!response.ok) {
console.warn('Could not load metadata file, using empty metadata');
this.metadataData = { metadata: {}, companies: {}, technologies: {} };
return this.metadataData;
}
this.metadataData = await response.json();
this.cache.set(cacheKey, this.metadataData);
return this.metadataData;
} catch (error) {
console.warn('Error loading metadata:', error);
this.metadataData = { metadata: {}, companies: {}, technologies: {} };
return this.metadataData;
}
}
// Get metadata for component (external metadata takes priority over frontmatter)
getComponentMetadata(componentName) {
if (!this.metadataData) {
return { tags: [], companies: [], technologies: [] };
}
const metadata = this.metadataData.metadata[componentName];
if (metadata) {
return {
tags: metadata.tags || [],
companies: metadata.companies || [],
technologies: metadata.technologies || []
};
}
// Fallback to empty metadata
return { tags: [], companies: [], technologies: [] };
}
// Extract tags from component frontmatter (kept as fallback)
extractTagsFromFrontmatter(content) {
if (!content) return { tags: [], companies: [], technologies: [] };
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) return { tags: [], companies: [], technologies: [] };
const frontmatter = frontmatterMatch[1];
// Extract tags array
const tagsMatch = frontmatter.match(/tags:\s*\[(.*?)\]/s);
const tags = tagsMatch
? tagsMatch[1].split(',').map(tag => tag.trim().replace(/['"]/g, ''))
: [];
// Extract companies array
const companiesMatch = frontmatter.match(/companies:\s*\[(.*?)\]/s);
const companies = companiesMatch
? companiesMatch[1].split(',').map(company => company.trim().replace(/['"]/g, ''))
: [];
// Extract technologies array
const technologiesMatch = frontmatter.match(/technologies:\s*\[(.*?)\]/s);
const technologies = technologiesMatch
? technologiesMatch[1].split(',').map(tech => tech.trim().replace(/['"]/g, ''))
: [];
return { tags, companies, technologies };
}
// Get all unique tags from components
getAllTags() {
const allTags = new Set();
this.getAllComponents().forEach(component => {
const { tags } = this.getComponentMetadata(component.name);
tags.forEach(tag => allTags.add(tag));
});
return Array.from(allTags).sort();
}
// Get all unique companies from components
getAllCompanies() {
const allCompanies = new Set();
this.getAllComponents().forEach(component => {
const { companies } = this.getComponentMetadata(component.name);
companies.forEach(company => allCompanies.add(company));
});
return Array.from(allCompanies).sort();
}
// Get all unique technologies from components
getAllTechnologies() {
const allTechnologies = new Set();
this.getAllComponents().forEach(component => {
const { technologies } = this.getComponentMetadata(component.name);
technologies.forEach(tech => allTechnologies.add(tech));
});
return Array.from(allTechnologies).sort();
}
// Get company info from metadata
getCompanyInfo(companySlug) {
if (!this.metadataData || !this.metadataData.companies) {
return null;
}
return this.metadataData.companies[companySlug] || null;
}
// Get technology info from metadata
getTechnologyInfo(techSlug) {
if (!this.metadataData || !this.metadataData.technologies) {
return null;
}
return this.metadataData.technologies[techSlug] || null;
}
// Filter components by tags, companies, or technologies
filterComponentsByTags(filterOptions = {}) {
const { tags = [], companies = [], technologies = [], type = null } = filterOptions;
let filteredComponents = this.getAllComponents();
// Filter by type if specified
if (type) {
filteredComponents = filteredComponents.filter(component => component.type === type);
}
// Filter by tags, companies, or technologies
if (tags.length > 0 || companies.length > 0 || technologies.length > 0) {
filteredComponents = filteredComponents.filter(component => {
const componentMeta = this.getComponentMetadata(component.name);
const hasMatchingTag = tags.length === 0 || tags.some(tag =>
componentMeta.tags.includes(tag)
);
const hasMatchingCompany = companies.length === 0 || companies.some(company =>
componentMeta.companies.includes(company)
);
const hasMatchingTechnology = technologies.length === 0 || technologies.some(tech =>
componentMeta.technologies.includes(tech)
);
return hasMatchingTag || hasMatchingCompany || hasMatchingTechnology;
});
}
return filteredComponents;
}
// Get components for a specific company stack
getCompanyStack(companySlug) {
const companyComponents = this.filterComponentsByTags({ companies: [companySlug] });
// Group by type
const groupedComponents = {
agents: companyComponents.filter(c => c.type === 'agent'),
commands: companyComponents.filter(c => c.type === 'command'),
mcps: companyComponents.filter(c => c.type === 'mcp'),
settings: companyComponents.filter(c => c.type === 'setting'),
hooks: companyComponents.filter(c => c.type === 'hook'),
templates: companyComponents.filter(c => c.type === 'template')
};
return groupedComponents;
}
// Get components for a specific technology stack
getTechnologyStack(techSlug) {
const techComponents = this.filterComponentsByTags({ technologies: [techSlug] });
// Group by type
const groupedComponents = {
agents: techComponents.filter(c => c.type === 'agent'),
commands: techComponents.filter(c => c.type === 'command'),
mcps: techComponents.filter(c => c.type === 'mcp'),
settings: techComponents.filter(c => c.type === 'setting'),
hooks: techComponents.filter(c => c.type === 'hook'),
templates: techComponents.filter(c => c.type === 'template')
};
return groupedComponents;
}
// Get all settings components
getSettings() {
if (!this.componentsData) return [];
return this.componentsData.settings || [];
}
// Get all hooks components
getHooks() {
if (!this.componentsData) return [];
return this.componentsData.hooks || [];
}
// Get settings by category
getSettingsByCategory(category) {
const settings = this.getSettings();
return settings.filter(setting => setting.category === category);
}
// Get hooks by category
getHooksByCategory(category) {
const hooks = this.getHooks();
return hooks.filter(hook => hook.category === category);
}
// Get all setting categories
getSettingCategories() {
const settings = this.getSettings();
const categories = new Set();
settings.forEach(setting => {
if (setting.category) {
categories.add(setting.category);
}
});
return Array.from(categories).sort();
}
// Get all hook categories
getHookCategories() {
const hooks = this.getHooks();
const categories = new Set();
hooks.forEach(hook => {
if (hook.category) {
categories.add(hook.category);
}
});
return Array.from(categories).sort();
}
}
// Global instance
window.dataLoader = new DataLoader();
+108
View File
@@ -0,0 +1,108 @@
// Event Tracker for Claude Code Templates Website
// Batches and sends website analytics events (search, cart, component views)
class EventTracker {
constructor() {
this.queue = [];
this.flushInterval = 30000; // 30 seconds
this.maxQueueSize = 20;
this.endpoint = '/api/track-website-events';
this.sessionId = this.getOrCreateSessionId();
this.visitorId = this.getOrCreateVisitorId();
this.screenWidth = window.screen?.width || null;
this.referrer = document.referrer || null;
this.timer = null;
this.startAutoFlush();
this.setupBeforeUnload();
}
getOrCreateSessionId() {
let id = sessionStorage.getItem('cct_session_id');
if (!id) {
id = this.generateId();
sessionStorage.setItem('cct_session_id', id);
}
return id;
}
getOrCreateVisitorId() {
let id = localStorage.getItem('cct_visitor_id');
if (!id) {
id = this.generateId();
localStorage.setItem('cct_visitor_id', id);
}
return id;
}
generateId() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
track(eventType, eventData) {
this.queue.push({
event_type: eventType,
event_data: eventData || {},
page_path: window.location.pathname,
timestamp: new Date().toISOString()
});
if (this.queue.length >= this.maxQueueSize) {
this.flush();
}
}
flush() {
if (this.queue.length === 0) return;
const events = this.queue.splice(0);
const payload = JSON.stringify({
events: events,
session_id: this.sessionId,
visitor_id: this.visitorId,
screen_width: this.screenWidth,
referrer: this.referrer
});
// Use sendBeacon for reliability (survives page unloads)
if (navigator.sendBeacon) {
const blob = new Blob([payload], { type: 'application/json' });
const sent = navigator.sendBeacon(this.endpoint, blob);
if (!sent) {
// Fallback to fetch if sendBeacon fails
this.sendViaFetch(payload);
}
} else {
this.sendViaFetch(payload);
}
}
sendViaFetch(payload) {
fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true
}).catch(() => {
// Silent failure - analytics should never break user experience
});
}
startAutoFlush() {
this.timer = setInterval(() => this.flush(), this.flushInterval);
}
setupBeforeUnload() {
window.addEventListener('beforeunload', () => this.flush());
// Also flush on visibility change (mobile tab switching)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
}
// Initialize global instance
window.eventTracker = new EventTracker();
+86
View File
@@ -0,0 +1,86 @@
// Generate search data files from components.json
// This script reads the main components.json and creates separate JSON files for each category
(async function generateSearchData() {
try {
// Read the main components.json file
const response = await fetch('components.json');
const data = await response.json();
console.log('Generating search data files...');
// Create separate files for each category
const categories = ['agents', 'commands', 'settings', 'hooks', 'mcps', 'templates'];
for (const category of categories) {
if (data[category] && Array.isArray(data[category])) {
console.log(`Processing ${category}: ${data[category].length} items`);
// Process each component to ensure search-friendly structure
const processedComponents = data[category].map(component => ({
...component,
// Add search-friendly fields
searchableText: [
component.name || component.title,
component.description,
component.category,
...(component.tags || []),
component.keywords || '',
component.path || ''
].filter(Boolean).join(' ').toLowerCase(),
// Normalize common fields
title: component.name || component.title,
displayName: (component.name || component.title || '').replace(/[-_]/g, ' '),
category: category,
// Add tags if not present
tags: component.tags || [component.category].filter(Boolean)
}));
// Save to individual file (simulate file creation)
// In a real scenario, this would write to the server filesystem
console.log(`Would save ${category}.json with ${processedComponents.length} components`);
// Try to cache in localStorage (may fail if quota exceeded)
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem(`searchData_${category}`, JSON.stringify(processedComponents));
} catch (e) {
// Quota exceeded — in-memory cache is sufficient
}
}
// Also store in a global variable for immediate use
window.searchDataCache = window.searchDataCache || {};
window.searchDataCache[category] = processedComponents;
}
}
console.log('Search data generation complete!');
} catch (error) {
console.error('Error generating search data:', error);
}
})();
// Function to get search data for a category
function getSearchData(category) {
// Try localStorage first
if (typeof localStorage !== 'undefined') {
const data = localStorage.getItem(`searchData_${category}`);
if (data) {
return JSON.parse(data);
}
}
// Try global cache
if (window.searchDataCache && window.searchDataCache[category]) {
return window.searchDataCache[category];
}
return [];
}
// Make function globally available
window.getSearchData = getSearchData;
File diff suppressed because it is too large Load Diff
+345
View File
@@ -0,0 +1,345 @@
// modal-helpers.js
// Show component modal
function showComponentModal(component) {
const modalHTML = createComponentModalHTML(component);
// Remove existing modal if present
const existingModal = document.querySelector('.modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Render code preview
renderCodePreview(component);
// Add event listener for ESC key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeComponentModal();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
// Create component modal HTML
function createComponentModalHTML(component) {
const typeConfig = {
agent: { icon: '🤖', color: '#ff6b6b', badge: 'AGENT' },
command: { icon: '⚡', color: '#4ecdc4', badge: 'COMMAND' },
mcp: { icon: '🔌', color: '#45b7d1', badge: 'MCP' },
template: { icon: '📦', color: '#f9a825', badge: 'TEMPLATE' }
};
const config = typeConfig[component.type] || typeConfig['template'];
// Generate install command - remove .md extension from path
let componentPath = component.path || component.name;
if (componentPath.endsWith('.md')) {
componentPath = componentPath.replace(/\.md$/, '');
}
if (componentPath.endsWith('.json')) {
componentPath = componentPath.replace(/\.json$/, '');
}
const installCommand = `npx claude-code-templates@latest --${component.type}=${componentPath} --yes`;
// Generate global agent command for agents only
const globalAgentCommand = component.type === 'agent' ? `npx claude-code-templates@latest --create-agent ${componentPath}` : null;
const description = getComponentDescription(component); // Full description
// Construct GitHub URL
let githubUrl = 'https://github.com/davila7/claude-code-templates/';
if (component.type === 'template') {
githubUrl += `tree/main/cli-tool/templates/${component.folderPath}`;
} else {
githubUrl += `blob/main/cli-tool/components/${component.type}s/${component.path}`;
}
return `
<div class="modal-overlay" onclick="closeComponentModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<div class="component-modal-title">
<span class="component-icon">${config.icon}</span>
<h3 style="color: var(--text-primary);">${formatComponentName(component.name)}</h3>
<div class="component-type-badge" style="background-color: ${config.color};">${config.badge}</div>
</div>
<button class="modal-close" onclick="closeComponentModal()">×</button>
</div>
<div class="modal-body">
<div class="component-details">
<div class="component-description">${component.type === 'mcp' ? (component.description || description) : description}</div>
<div class="installation-section">
<!-- Basic Installation -->
<div class="basic-installation-section">
<h4>📦 Basic Installation</h4>
<p class="installation-description">Install this ${component.type} locally in your project. Works with your existing Claude Code setup.</p>
<div class="command-line">
<code>${installCommand}</code>
<button class="copy-btn" data-command="${installCommand.replace(/"/g, '&quot;')}" onclick="copyToClipboard(this.dataset.command)">Copy</button>
</div>
</div>
${globalAgentCommand ? `
<!-- Global Agent (Claude Code SDK) -->
<div class="global-agent-section">
<h4>🌍 Global Agent (Claude Code SDK)</h4>
<p class="global-agent-description">Create a global AI agent accessible from anywhere with zero configuration. Perfect for automation and CI/CD workflows.</p>
<div class="command-line">
<code>${globalAgentCommand}</code>
<button class="copy-btn" data-command="${globalAgentCommand.replace(/"/g, '&quot;')}" onclick="copyToClipboard(this.dataset.command)">Copy</button>
</div>
<div class="global-agent-usage">
<div class="usage-example">
<div class="usage-title">After installation, use from anywhere:</div>
<div class="command-line usage-command">
<code>${componentPath.split('/').pop()} "your prompt here"</code>
<button class="copy-btn" data-command="${componentPath.split('/').pop()} &quot;your prompt here&quot;" onclick="copyToClipboard(this.dataset.command)">Copy</button>
</div>
</div>
<div class="global-features">
<div class="feature">✅ Works in scripts, CI/CD, npm tasks</div>
<div class="feature">✅ Auto-detects project context</div>
<div class="feature">✅ Powered by Claude Code SDK</div>
</div>
</div>
</div>` : ''}
${component.type === 'agent' ? `
<!-- Run in E2B Sandbox (Cloud Execution) -->
<div class="e2b-sandbox-section">
<h4>☁️ Run in E2B Sandbox (Cloud Execution) <span class="new-badge">NEW</span></h4>
<p class="e2b-description">Execute Claude Code with this ${component.type} in an isolated cloud environment using E2B. Perfect for testing complex projects without affecting your local system.</p>
<div class="e2b-api-setup">
<h5>🔑 Setup API Keys</h5>
<div class="env-setup-simple">
<div class="env-comment">Add to your .env file:</div>
<div class="env-example">
<code>E2B_API_KEY=your_e2b_key_here</code>
<code>ANTHROPIC_API_KEY=your_anthropic_key_here</code>
</div>
</div>
<div class="command-line">
<code>npx claude-code-templates@latest --sandbox e2b --agent=${componentPath} --prompt "your development task"</code>
<button class="copy-btn" data-command="npx claude-code-templates@latest --sandbox e2b --agent=${componentPath} --prompt &quot;your development task&quot;" onclick="copyToClipboard(this.dataset.command)">Copy</button>
</div>
</div>
<div class="e2b-features">
<div class="feature">🔒 Isolated cloud environment</div>
<div class="feature">⚡ Extended timeouts for complex operations</div>
<div class="feature">📁 Files downloaded to project root</div>
<div class="feature">🔍 Real-time execution monitoring</div>
</div>
<div class="e2b-requirements">
<div class="requirements-title">Get API Keys:</div>
<div class="api-key-links">
<a href="https://e2b.dev/dashboard" target="_blank" class="api-key-link">
<span>🔑</span> E2B API Key
</a>
<a href="https://console.anthropic.com" target="_blank" class="api-key-link">
<span>🔑</span> Anthropic API Key
</a>
</div>
</div>
</div>` : ''}
</div>
<div class="component-content">
<h4>📋 Component Details</h4>
<div class="code-editor" id="code-editor">
<div class="code-line-numbers" id="line-numbers"></div>
<div class="code-content">
<pre id="code-viewer"><code>Loading content...</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="add-to-cart-btn"
data-type="${component.type}s"
data-path="${componentPath}"
onclick="handleAddToCart('${component.name.replace(/'/g, "\\'")}', '${componentPath.replace(/'/g, "\\'")}', '${component.type}s', '${component.category ? component.category.replace(/'/g, "\\'") : 'Other'}', this); closeComponentModal();">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19,7H18V6A2,2 0 0,0 16,4H8A2,2 0 0,0 6,6V7H5A1,1 0 0,0 4,8A1,1 0 0,0 5,9H6V19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V9H19A1,1 0 0,0 20,8A1,1 0 0,0 19,7M8,6H16V7H8V6M16,19H8V9H16V19Z"/>
</svg>
Add to Stack
</button>
<a href="${githubUrl}" target="_blank" class="github-folder-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.30.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
</div>
</div>
</div>
`;
}
// Render code preview in the modal
function renderCodePreview(component) {
const viewer = document.getElementById('code-viewer');
const lineNumbers = document.getElementById('line-numbers');
if (!viewer) return;
let content = component.content || "No content available.";
let language = 'plaintext';
if (component.path) {
const extension = component.path.split('.').pop();
switch (extension) {
case 'md':
language = 'markdown';
break;
case 'json':
language = 'json';
// Pretty print JSON
try {
content = JSON.stringify(JSON.parse(content), null, 2);
} catch (e) { /* Ignore parsing errors */ }
break;
case 'js':
language = 'javascript';
break;
case 'yml':
case 'yaml':
language = 'yaml';
break;
}
}
// Show only a preview (first 15 lines) instead of full content
const lines = content.split('\n');
const previewLines = lines.slice(0, 15);
const previewContent = previewLines.join('\n');
// Add truncation indicator if content is longer
const truncatedContent = lines.length > 15 ? previewContent + '\n...' : previewContent;
const codeElement = viewer.querySelector('code');
codeElement.innerHTML = highlightCode(truncatedContent, language);
codeElement.className = `language-${language}`;
// Generate line numbers for preview
if (lineNumbers) {
const previewLineNumbers = previewLines.map((_, index) => `<span>${index + 1}</span>`).join('');
lineNumbers.innerHTML = previewLineNumbers;
}
}
// Basic syntax highlighting
function highlightCode(content, language) {
// First escape HTML entities
let highlighted = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (language === 'markdown' || language === 'yaml') {
// Highlight YAML/Markdown frontmatter keys (blue)
highlighted = highlighted.replace(/^([a-zA-Z_-]+):/gm, '<span style="color: #569cd6;">$1</span>:');
// Highlight strings in quotes (orange)
highlighted = highlighted.replace(/&quot;([^&]+?)&quot;/g, '<span style="color: #ce9178;">&quot;$1&quot;</span>');
// Highlight important keywords (light blue - like in the image)
highlighted = highlighted.replace(/\b(hackathon|strategy|AI|solution|ideation|evaluation|projects|feedback|concepts|feasibility|guidance|agent|specialist|brainstorming|winning|judge|feedback|Context|User|Examples)\b/gi, '<span style="color: #4fc1ff;">$1</span>');
// Highlight markdown headers (blue)
highlighted = highlighted.replace(/^(#+)\s*(.+)$/gm, '<span style="color: #569cd6;">$1</span> <span style="color: #dcdcaa;">$2</span>');
// Highlight code in backticks
highlighted = highlighted.replace(/`([^`]+)`/g, '<span style="color: #ce9178;">$1</span>');
// Highlight YAML separators
highlighted = highlighted.replace(/^---$/gm, '<span style="color: #808080;">---</span>');
}
return highlighted;
}
// Close component modal
function closeComponentModal() {
const modalOverlay = document.querySelector('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
// Utility to get component description
function getComponentDescription(component, maxLength = 0) {
let description = component.description || '';
if (!description && component.content) {
const descMatch = component.content.match(/description:\s*(.+?)(?:\n|$)/);
if (descMatch) {
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
} else {
const lines = component.content.split('\n');
const firstParagraph = lines.find(line => line.trim() && !line.startsWith('---') && !line.startsWith('#'));
if (firstParagraph) {
description = firstParagraph.trim();
}
}
}
if (!description) {
description = `A ${component.type} component.`;
}
if (maxLength && description.length > maxLength) {
description = description.substring(0, maxLength - 3) + '...';
}
return description;
}
// Utility to format component name
function formatComponentName(name) {
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
// Global function for component details (called from onclick)
function showComponentDetails(type, name, path, category) {
// Instead of showing modal, redirect to component page
const componentURL = createComponentURL(type, name, path);
window.location.href = componentURL;
}
// Function to create component URL
function createComponentURL(type, name, path) {
// Detect if we're in local development
const isLocal = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname.includes('5500');
if (!isLocal && type && name) {
// Use SEO-friendly URL structure for production: /component/type/name
let cleanName = name;
if (cleanName.endsWith('.md')) {
cleanName = cleanName.slice(0, -3);
}
if (cleanName.endsWith('.json')) {
cleanName = cleanName.slice(0, -5);
}
return `component/${encodeURIComponent(type)}/${encodeURIComponent(cleanName)}`;
}
// Use query parameters for local development or fallback
const params = new URLSearchParams();
params.set('type', type);
if (name) params.set('name', name);
if (path) params.set('path', path);
return `component.html?${params.toString()}`;
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Partnership Banner - GLM Z.AI
* Handles banner click tracking
*/
(function() {
'use strict';
/**
* Initialize partnership banner
*/
function initPartnershipBanner() {
// Track CTA clicks
const primaryCTA = document.getElementById('partnershipPrimaryCTA');
if (primaryCTA) {
primaryCTA.addEventListener('click', function(e) {
trackEvent('partnership_click', {
partner: 'z.ai',
action: 'subscribe_link',
component: 'glm-coding-plan'
});
});
}
}
/**
* Track analytics event
*/
function trackEvent(eventName, params) {
// Google Analytics (gtag)
if (typeof gtag === 'function') {
gtag('event', eventName, params);
}
// Console log for debugging
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.log('Partnership Banner Event:', eventName, params);
}
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPartnershipBanner);
} else {
initPartnershipBanner();
}
})();
+390
View File
@@ -0,0 +1,390 @@
/**
* Plugin Detail Page Manager
* Handles loading and displaying plugin details
*/
class PluginPageManager {
constructor() {
this.pluginData = null;
this.allPlugins = [];
this.pluginName = this.getPluginNameFromURL();
}
/**
* Get plugin name from URL path
*/
getPluginNameFromURL() {
const path = window.location.pathname;
const segments = path.split('/').filter(segment => segment);
// URL format: /plugin/plugin-name
if (segments[0] === 'plugin' && segments[1]) {
return segments[1];
}
return null;
}
/**
* Initialize the page
*/
async init() {
if (!this.pluginName) {
this.showError();
return;
}
try {
// Load components data
await this.loadComponentsData();
// Find the plugin
this.pluginData = this.findPlugin(this.pluginName);
if (!this.pluginData) {
this.showError();
return;
}
// Render the plugin details
this.renderPluginDetails();
this.renderRelatedPlugins();
// Update page title
document.title = `${this.formatPluginName(this.pluginData.name)} - Claude Code Templates`;
// Setup keyboard listeners
this.setupKeyboardListeners();
} catch (error) {
console.error('Error loading plugin:', error);
this.showError();
}
}
/**
* Setup keyboard event listeners
*/
setupKeyboardListeners() {
document.addEventListener('keydown', (e) => {
// ESC key closes modal
if (e.key === 'Escape') {
const modal = document.getElementById('installModal');
if (modal.style.display === 'flex') {
this.closeInstallModal();
}
}
});
}
/**
* Load components.json
*/
async loadComponentsData() {
try {
const response = await fetch('/components.json');
if (!response.ok) {
throw new Error('Failed to load components data');
}
const data = await response.json();
this.allPlugins = data.plugins || [];
} catch (error) {
console.error('Error loading components:', error);
throw error;
}
}
/**
* Find plugin by name
*/
findPlugin(name) {
return this.allPlugins.find(plugin => plugin.name === name);
}
/**
* Format name (convert kebab-case to Title Case)
*/
formatPluginName(name) {
return this.formatComponentName(name);
}
/**
* Format component name (convert kebab-case to Title Case)
* Handles both "name" and "category/name" formats
*/
formatComponentName(name) {
// Extract just the filename if it includes a category path
const displayName = name.includes('/') ? name.split('/').pop() : name;
return displayName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Render plugin details
*/
renderPluginDetails() {
const plugin = this.pluginData;
// Update hero section
document.getElementById('pluginName').textContent = this.formatPluginName(plugin.name);
document.getElementById('pluginDescription').textContent = plugin.description;
document.getElementById('pluginVersion').textContent = `v${plugin.version}`;
document.getElementById('pluginAuthor').textContent = plugin.author || 'Claude Code Templates';
// Render keywords
const keywordsContainer = document.getElementById('pluginKeywords');
keywordsContainer.innerHTML = plugin.keywords
.map(keyword => `<span class="keyword-badge">${keyword}</span>`)
.join('');
// Update installation commands
document.getElementById('installPluginCmd').textContent = `/plugin install ${plugin.name}@claude-code-templates`;
// Render components sections
this.renderComponents(plugin);
// Show content, hide loading
document.getElementById('loading').style.display = 'none';
document.getElementById('pluginContent').style.display = 'block';
}
/**
* Render components (commands, agents, MCPs)
*/
renderComponents(plugin) {
// Commands
if (plugin.commands && plugin.commands > 0) {
document.getElementById('commandsSection').style.display = 'block';
document.getElementById('commandsCount').textContent = plugin.commands;
const commandsList = document.getElementById('commandsList');
commandsList.innerHTML = this.generateComponentItems(
plugin.commandsList || [],
'command',
'commands'
);
}
// Agents
if (plugin.agents && plugin.agents > 0) {
document.getElementById('agentsSection').style.display = 'block';
document.getElementById('agentsCount').textContent = plugin.agents;
const agentsList = document.getElementById('agentsList');
agentsList.innerHTML = this.generateComponentItems(
plugin.agentsList || [],
'agent',
'agents'
);
}
// MCPs
if (plugin.mcpServers && plugin.mcpServers > 0) {
document.getElementById('mcpsSection').style.display = 'block';
document.getElementById('mcpsCount').textContent = plugin.mcpServers;
const mcpsList = document.getElementById('mcpsList');
mcpsList.innerHTML = this.generateComponentItems(
plugin.mcpServersList || [],
'mcp',
'mcps'
);
}
}
/**
* Generate component items with install commands
*/
generateComponentItems(componentNames, componentType, pluralType) {
if (!componentNames || componentNames.length === 0) {
return '<li>No components available</li>';
}
return componentNames.map((name, index) => {
const formattedName = this.formatComponentName(name);
const installCommand = `npx claude-code-templates@latest --${componentType} ${name}`;
return `
<li class="component-item" data-index="${index}">
<span class="component-name">${formattedName}</span>
<button class="component-install-btn" onclick="window.pluginPageManager.showInstallCommand('${installCommand.replace(/'/g, "\\'")}', event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
Install
</button>
</li>
`;
}).join('');
}
/**
* Show install command in a modal
*/
showInstallCommand(command, event) {
event.stopPropagation();
// Store current command
this.currentCommand = command;
// Update modal content
document.getElementById('modalCommand').textContent = command;
// Show modal
const modal = document.getElementById('installModal');
modal.style.display = 'flex';
// Prevent body scroll
document.body.style.overflow = 'hidden';
}
/**
* Close install modal
*/
closeInstallModal() {
const modal = document.getElementById('installModal');
modal.style.display = 'none';
// Restore body scroll
document.body.style.overflow = 'auto';
}
/**
* Copy command from modal
*/
copyModalCommand() {
const command = this.currentCommand;
navigator.clipboard.writeText(command).then(() => {
const button = document.querySelector('.modal-copy-btn');
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
</svg>
Copied!
`;
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
/**
* Render related plugins (based on shared keywords)
*/
renderRelatedPlugins() {
const currentPlugin = this.pluginData;
const relatedPlugins = this.findRelatedPlugins(currentPlugin);
const container = document.getElementById('relatedPlugins');
if (relatedPlugins.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary);">No related plugins found.</p>';
return;
}
container.innerHTML = relatedPlugins
.map(plugin => this.createRelatedPluginCard(plugin))
.join('');
}
/**
* Find related plugins based on shared keywords
*/
findRelatedPlugins(currentPlugin) {
const currentKeywords = new Set(currentPlugin.keywords);
return this.allPlugins
.filter(plugin => plugin.name !== currentPlugin.name)
.map(plugin => {
// Count shared keywords
const sharedKeywords = plugin.keywords.filter(k => currentKeywords.has(k)).length;
return { plugin, sharedKeywords };
})
.filter(item => item.sharedKeywords > 0)
.sort((a, b) => b.sharedKeywords - a.sharedKeywords)
.slice(0, 3) // Show top 3 related plugins
.map(item => item.plugin);
}
/**
* Create related plugin card HTML
*/
createRelatedPluginCard(plugin) {
return `
<a href="/plugin/${plugin.name}" class="related-plugin-card">
<h3>${this.formatPluginName(plugin.name)}</h3>
<p>${plugin.description}</p>
<div class="related-plugin-stats">
${plugin.commands > 0 ? `<span class="related-plugin-stat"><span class="stat-icon">⚡</span>${plugin.commands}</span>` : ''}
${plugin.agents > 0 ? `<span class="related-plugin-stat"><span class="stat-icon">🤖</span>${plugin.agents}</span>` : ''}
${plugin.mcpServers > 0 ? `<span class="related-plugin-stat"><span class="stat-icon">🔌</span>${plugin.mcpServers}</span>` : ''}
</div>
</a>
`;
}
/**
* Show error state
*/
showError() {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'flex';
}
}
/**
* Copy command to clipboard
*/
function copyCommand(elementId, event) {
const element = document.getElementById(elementId);
const text = element.textContent;
navigator.clipboard.writeText(text).then(() => {
// Show feedback - find the button that was clicked
let button = null;
if (event && event.target) {
button = event.target.closest('.copy-btn');
}
// If we can't find the button through event, try to find it near the element
if (!button) {
const parentContainer = element.closest('.command-box');
if (parentContainer) {
button = parentContainer.querySelector('.copy-btn');
}
}
if (button) {
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
</svg>
Copied!
`;
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.pluginPageManager = new PluginPageManager();
window.pluginPageManager.init();
});
+2194
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+555
View File
@@ -0,0 +1,555 @@
// Stack Router - Handles company and technology specific stack pages
class StackRouter {
constructor() {
this.routes = new Map();
this.currentRoute = null;
this.init();
}
init() {
// Listen for hash changes and initial load
window.addEventListener('hashchange', () => this.handleRouteChange());
window.addEventListener('load', () => this.handleRouteChange());
// Check for path-based routes on load
this.handleRouteChange();
}
// Get company info from data loader
getCompanyInfo(slug) {
return window.dataLoader.getCompanyInfo(slug);
}
// Get technology info from data loader
getTechnologyInfo(slug) {
return window.dataLoader.getTechnologyInfo(slug);
}
// Handle route changes
handleRouteChange() {
const path = window.location.pathname;
const hash = window.location.hash;
// Check for company routes (/company/epic-games)
const companyMatch = path.match(/\/company\/([^\/]+)/);
if (companyMatch) {
this.loadCompanyStack(companyMatch[1]);
return;
}
// Check for technology routes (/technology/unity)
const technologyMatch = path.match(/\/technology\/([^\/]+)/);
if (technologyMatch) {
this.loadTechnologyStack(technologyMatch[1]);
return;
}
// Check for hash-based routes for development
if (hash.startsWith('#/company/')) {
const company = hash.replace('#/company/', '');
this.loadCompanyStack(company);
return;
}
if (hash.startsWith('#/technology/')) {
const technology = hash.replace('#/technology/', '');
this.loadTechnologyStack(technology);
return;
}
if (hash === '#/companies') {
this.loadAllCompaniesPage();
return;
}
// Return to main page
this.loadMainPage();
}
// Load company-specific stack page
async loadCompanyStack(companySlug) {
const companyInfo = this.getCompanyInfo(companySlug);
if (!companyInfo) {
console.error('Company not found:', companySlug);
return;
}
this.currentRoute = { type: 'company', slug: companySlug, info: companyInfo };
// Wait for data to be loaded
if (!window.dataLoader.componentsData) {
await window.dataLoader.loadAllComponents();
}
// Get company stack components
const stackComponents = window.dataLoader.getCompanyStack(companySlug);
// Update page content
this.renderStackPage(companyInfo, stackComponents, 'company');
// Update page title and meta
document.title = `${companyInfo.name} Stack - Claude Code Templates`;
this.updateMetaTags(companyInfo, 'company');
}
// Load technology-specific stack page
async loadTechnologyStack(techSlug) {
const techInfo = this.getTechnologyInfo(techSlug);
if (!techInfo) {
console.error('Technology not found:', techSlug);
return;
}
this.currentRoute = { type: 'technology', slug: techSlug, info: techInfo };
// Wait for data to be loaded
if (!window.dataLoader.componentsData) {
await window.dataLoader.loadAllComponents();
}
// Get technology stack components
const stackComponents = window.dataLoader.getTechnologyStack(techSlug);
// Update page content
this.renderStackPage(techInfo, stackComponents, 'technology');
// Update page title and meta
document.title = `${techInfo.name} Stack - Claude Code Templates`;
this.updateMetaTags(techInfo, 'technology');
}
// Render stack page content
renderStackPage(stackInfo, components, type) {
const main = document.querySelector('main.terminal');
if (!main) return;
// Hide original content
const originalSections = main.querySelectorAll('section:not(.stack-page)');
originalSections.forEach(section => section.style.display = 'none');
// Remove existing stack page if any
const existingStackPage = main.querySelector('.stack-page');
if (existingStackPage) {
existingStackPage.remove();
}
// Create stack page
const stackPageHTML = this.generateStackPageHTML(stackInfo, components, type);
main.insertAdjacentHTML('afterbegin', stackPageHTML);
// Initialize cart functionality for the new page
if (window.initializeCartForStackPage) {
window.initializeCartForStackPage();
}
// Update header to show back button
this.updateHeaderForStack(stackInfo);
}
// Generate stack page HTML
generateStackPageHTML(stackInfo, components, type) {
const totalComponents = Object.values(components).reduce((sum, arr) => sum + arr.length, 0);
return `
<section class="stack-page">
<!-- Stack Header -->
<div class="stack-header">
<div class="stack-info">
<div class="stack-logo">${stackInfo.logo}</div>
<div class="stack-details">
<h1>${stackInfo.name} Development Stack</h1>
<p class="stack-description">${stackInfo.description}</p>
<div class="stack-stats">
<div class="stack-stat">
<span class="stat-number">${totalComponents}</span>
<span class="stat-label">Components</span>
</div>
<div class="stack-stat">
<span class="stat-number">${components.agents.length}</span>
<span class="stat-label">Agents</span>
</div>
<div class="stack-stat">
<span class="stat-number">${components.commands.length}</span>
<span class="stat-label">Commands</span>
</div>
<div class="stack-stat">
<span class="stat-number">${components.mcps.length}</span>
<span class="stat-label">MCPs</span>
</div>
</div>
${stackInfo.website ? `<a href="${stackInfo.website}" target="_blank" class="stack-website">Visit ${stackInfo.name} →</a>` : ''}
</div>
</div>
</div>
<!-- Stack Components -->
<div class="stack-components">
${this.generateStackComponentsHTML(components)}
</div>
<!-- Install All Section -->
<div class="stack-install-all">
<h3>🚀 Install Complete ${stackInfo.name} Stack</h3>
<p>Get all ${totalComponents} components with a single command:</p>
<div class="command-line">
<span class="prompt">$</span>
<code class="command" id="stackInstallCommand">${this.generateStackInstallCommand(components)}</code>
<button class="copy-btn" onclick="copyToClipboard(document.getElementById('stackInstallCommand').textContent)">Copy</button>
</div>
</div>
</section>
`;
}
// Generate components sections HTML
generateStackComponentsHTML(components) {
let html = '';
const sections = [
{ type: 'agents', title: 'AI Agents', icon: '🤖', description: 'Specialized AI assistants for your workflow' },
{ type: 'commands', title: 'Commands', icon: '⚡', description: 'Ready-to-use automation commands' },
{ type: 'mcps', title: 'MCPs', icon: '🔌', description: 'Model Context Protocol integrations' }
];
sections.forEach(section => {
const items = components[section.type] || [];
if (items.length > 0) {
html += `
<div class="stack-section">
<div class="stack-section-header">
<h3>${section.icon} ${section.title}</h3>
<p>${section.description}</p>
<span class="component-count">${items.length} ${items.length === 1 ? section.type.slice(0, -1) : section.type}</span>
</div>
<div class="stack-grid">
${items.map(component => this.generateComponentCardHTML(component)).join('')}
</div>
</div>
`;
}
});
return html;
}
// Generate component card HTML
generateComponentCardHTML(component) {
const { tags, companies, technologies } = window.dataLoader.getComponentMetadata(component.name);
return `
<div class="component-card stack-component" data-name="${component.name}" data-type="${component.type}">
<div class="component-header">
<h4>${component.name}</h4>
<div class="component-actions">
<button class="add-to-cart-btn" onclick="addToCart('${component.name}', '${component.type}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19,7H18V6A6,6 0 0,0 6,6V7H5A1,1 0 0,0 4,8V19A3,3 0 0,0 7,22H17A3,3 0 0,0 20,19V8A1,1 0 0,0 19,7M12,2A4,4 0 0,1 16,6V7H8V6A4,4 0 0,1 12,2M7,20A1,1 0 0,1 6,19V9H8V11A1,1 0 0,0 10,11A1,1 0 0,0 10,9V9H14V11A1,1 0 0,0 16,11A1,1 0 0,0 16,9V9H18V19A1,1 0 0,1 17,20H7M12,13A1,1 0 0,0 11,14A1,1 0 0,0 12,15A1,1 0 0,0 13,14A1,1 0 0,0 12,13Z"/>
</svg>
Add
</button>
</div>
</div>
<div class="component-content">
${this.extractDescription(component.content)}
</div>
${tags.length > 0 || technologies.length > 0 ? `
<div class="component-tags">
${[...tags.slice(0, 3), ...technologies.slice(0, 2)].map(tag =>
`<span class="component-tag">${tag}</span>`
).join('')}
</div>
` : ''}
</div>
`;
}
// Extract description from component content
extractDescription(content) {
const descMatch = content.match(/description:\s*(.+?)(?:\n|$)/);
if (descMatch) {
return descMatch[1].replace(/^['"]|['"]$/g, '').substring(0, 150) + '...';
}
return 'No description available';
}
// Generate install command for entire stack
generateStackInstallCommand(components) {
const allComponents = [...components.agents, ...components.commands, ...components.mcps];
const componentArgs = allComponents.map(c => `--${c.type} ${c.name}`).join(' ');
return `npx claude-code-templates@latest ${componentArgs}`;
}
// Update header for stack page
updateHeaderForStack(stackInfo) {
const header = document.querySelector('.header');
if (!header) return;
// Add back button
let backButton = header.querySelector('.back-button');
if (!backButton) {
backButton = document.createElement('button');
backButton.className = 'back-button';
backButton.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"/>
</svg>
Back to Main
`;
backButton.onclick = () => this.goBack();
const headerContent = header.querySelector('.header-content');
if (headerContent) {
headerContent.insertBefore(backButton, headerContent.firstChild);
}
}
}
// Update meta tags for SEO
updateMetaTags(stackInfo, type) {
// Update Open Graph tags
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDescription = document.querySelector('meta[property="og:description"]');
if (ogTitle) {
ogTitle.content = `${stackInfo.name} Development Stack - Claude Code Templates`;
}
if (ogDescription) {
ogDescription.content = stackInfo.description;
}
// Update Twitter tags
const twitterTitle = document.querySelector('meta[name="twitter:title"]');
const twitterDescription = document.querySelector('meta[name="twitter:description"]');
if (twitterTitle) {
twitterTitle.content = `${stackInfo.name} Development Stack - Claude Code Templates`;
}
if (twitterDescription) {
twitterDescription.content = stackInfo.description;
}
}
// Navigate back to main page
goBack() {
// Remove stack page
const stackPage = document.querySelector('.stack-page');
if (stackPage) {
stackPage.remove();
}
// Show original content
const main = document.querySelector('main.terminal');
if (main) {
const originalSections = main.querySelectorAll('section:not(.stack-page)');
originalSections.forEach(section => section.style.display = '');
}
// Remove back button
const backButton = document.querySelector('.back-button');
if (backButton) {
backButton.remove();
}
// Reset route
this.currentRoute = null;
window.history.pushState({}, '', window.location.pathname);
// Reset page title and meta
document.title = 'Claude Code Templates';
this.resetMetaTags();
}
// Load main page (reset everything)
loadMainPage() {
if (this.currentRoute) {
this.goBack();
}
}
// Reset meta tags to original values
resetMetaTags() {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDescription = document.querySelector('meta[property="og:description"]');
if (ogTitle) {
ogTitle.content = 'Claude Code Templates - Ready-to-use configurations';
}
if (ogDescription) {
ogDescription.content = 'Browse and install Claude Code configuration templates for different languages and frameworks. Includes 100+ agents, 159+ commands, 23+ MCPs, and 14+ templates.';
}
}
// Load all companies page
async loadAllCompaniesPage() {
this.currentRoute = { type: 'companies', slug: 'all' };
// Wait for data to be loaded
if (!window.dataLoader.componentsData) {
await window.dataLoader.loadAllComponents();
}
// Update page content
this.renderAllCompaniesPage();
// Update page title and meta
document.title = 'All Development Stacks - Claude Code Templates';
this.updateMetaTagsForAllCompanies();
}
// Render all companies page
renderAllCompaniesPage() {
const main = document.querySelector('main.terminal');
if (!main) return;
// Hide original content
const originalSections = main.querySelectorAll('section:not(.stack-page)');
originalSections.forEach(section => section.style.display = 'none');
// Remove existing stack page if any
const existingStackPage = main.querySelector('.stack-page');
if (existingStackPage) {
existingStackPage.remove();
}
// Get all companies from metadata
const allCompanies = window.dataLoader.metadataData?.companies || {};
// Create all companies page
const allCompaniesHTML = this.generateAllCompaniesPageHTML(allCompanies);
main.insertAdjacentHTML('afterbegin', allCompaniesHTML);
// Update header to show back button
this.updateHeaderForStack({ name: 'All Development Stacks' });
}
// Generate all companies page HTML
generateAllCompaniesPageHTML(companies) {
const companiesArray = Object.entries(companies);
// Group companies by category
const categories = {
'AI & Machine Learning': ['openai', 'anthropic'],
'Payments & E-commerce': ['stripe', 'shopify', 'shopify'],
'CRM & Business': ['salesforce', 'hubspot', 'airtable', 'linear'],
'Communication': ['twilio', 'slack', 'discord', 'sendgrid'],
'Cloud & Infrastructure': ['aws', 'vercel', 'netlify', 'cloudflare', 'firebase', 'supabase'],
'Databases': ['mongodb', 'planetscale'],
'Development Tools': ['github', 'figma', 'adobe', 'atlassian', 'notion'],
'Entertainment & Media': ['spotify', 'youtube', 'twitter'],
'Game Development': ['unity-technologies', 'epic-games'],
'Marketing': ['mailchimp', 'hubspot']
};
let categorizedHTML = '';
let uncategorizedCompanies = [...companiesArray];
// Generate categorized sections
Object.entries(categories).forEach(([categoryName, companyIds]) => {
const categoryCompanies = companyIds
.map(id => companiesArray.find(([key]) => key === id))
.filter(Boolean);
if (categoryCompanies.length > 0) {
categorizedHTML += `
<div class="companies-category">
<h3 class="category-title">${categoryName}</h3>
<div class="companies-grid">
${categoryCompanies.map(([key, company]) => {
// Remove from uncategorized list
uncategorizedCompanies = uncategorizedCompanies.filter(([k]) => k !== key);
return this.generateCompanyCardHTML(key, company);
}).join('')}
</div>
</div>
`;
}
});
// Add uncategorized companies if any
if (uncategorizedCompanies.length > 0) {
categorizedHTML += `
<div class="companies-category">
<h3 class="category-title">Other Platforms</h3>
<div class="companies-grid">
${uncategorizedCompanies.map(([key, company]) =>
this.generateCompanyCardHTML(key, company)
).join('')}
</div>
</div>
`;
}
return `
<section class="stack-page all-companies-page">
<div class="stack-header">
<div class="stack-info">
<div class="stack-logo">🏢</div>
<div class="stack-details">
<h1>All Development Stacks</h1>
<p class="stack-description">Complete list of companies and platforms with development APIs, SDKs, and integration opportunities</p>
<div class="stack-stats">
<div class="stack-stat">
<span class="stat-number">${companiesArray.length}</span>
<span class="stat-label">Companies</span>
</div>
<div class="stack-stat">
<span class="stat-number">${Object.keys(categories).length}</span>
<span class="stat-label">Categories</span>
</div>
</div>
</div>
</div>
</div>
<div class="all-companies-content">
${categorizedHTML}
</div>
</section>
`;
}
// Generate individual company card for all companies page
generateCompanyCardHTML(companyKey, company) {
return `
<a href="#/company/${companyKey}" class="all-company-card" onclick="window.stackRouter?.navigateTo('#/company/${companyKey}')">
<div class="company-card-logo">${company.logo}</div>
<div class="company-card-info">
<h4>${company.name}</h4>
<p>${company.description}</p>
</div>
<div class="company-card-arrow">→</div>
</a>
`;
}
// Update meta tags for all companies page
updateMetaTagsForAllCompanies() {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDescription = document.querySelector('meta[property="og:description"]');
if (ogTitle) {
ogTitle.content = 'All Development Stacks - Claude Code Templates';
}
if (ogDescription) {
ogDescription.content = 'Browse all available development stacks for major companies and platforms. Find agents, commands, and MCPs for APIs like OpenAI, Stripe, Shopify, AWS, and more.';
}
}
// Navigate programmatically
navigateTo(path) {
window.history.pushState({}, '', path);
this.handleRouteChange();
}
}
// Initialize router when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.stackRouter = new StackRouter();
});
// Make it available globally
window.StackRouter = StackRouter;
+794
View File
@@ -0,0 +1,794 @@
/**
* Trending Page JavaScript
* GitHub-inspired trending components page for Claude Code Templates
*/
class TrendingPage {
constructor() {
this.currentType = 'agents';
this.currentRange = 'month'; // Changed from 'today' to 'month' to show real data
this.data = null;
this.init();
}
async init() {
try {
await this.loadData();
this.setupEventListeners();
this.renderHeroStats();
this.renderPopularItems();
this.renderTopCountries();
this.renderChart();
this.renderTrendingItems();
} catch (error) {
console.error('Failed to initialize trending page:', error);
this.showError('Failed to load trending data');
}
}
async loadData() {
try {
const response = await fetch('trending-data.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.data = await response.json();
} catch (error) {
console.error('Error loading trending data:', error);
throw error;
}
}
setupEventListeners() {
// Component tabs
document.querySelectorAll('.component-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
// Remove active class from all tabs
document.querySelectorAll('.component-tab').forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
e.target.classList.add('active');
// Update current type
this.currentType = e.target.dataset.type;
this.renderTrendingItems();
});
});
// Date range filter
const dateRange = document.getElementById('date-range');
if (dateRange) {
dateRange.addEventListener('change', (e) => {
this.currentRange = e.target.value;
this.renderTrendingItems();
});
}
}
renderHeroStats() {
const container = document.getElementById('hero-stats');
if (!container || !this.data || !this.data.globalStats) {
return;
}
// Use real global statistics from the JSON
const globalStats = this.data.globalStats;
const stats = [
{
number: globalStats.totalComponents.toLocaleString(),
label: 'Unique Components',
change: 'Across 7 categories',
positive: true
},
{
number: globalStats.totalDownloads.toLocaleString(),
label: 'Total Downloads',
change: 'All time downloads',
positive: true
},
{
number: globalStats.totalCountries.toLocaleString(),
label: 'Countries',
change: 'Global reach',
positive: true
}
];
container.innerHTML = stats.map(stat => `
<div class="hero-stat-item">
<div class="hero-stat-number">${stat.number}</div>
<div class="hero-stat-label">${stat.label}</div>
<div class="hero-stat-change ${stat.positive ? '' : 'negative'}">
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
<path d="${stat.positive ? 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z' : 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM3.25 8a.75.75 0 0 1 .75-.75h8a.75.75 0 0 1 0 1.5h-8A.75.75 0 0 1 3.25 8z'}"/>
</svg>
${stat.change}
</div>
</div>
`).join('');
}
renderPopularItems() {
const container = document.getElementById('popular-categories');
if (!container || !this.data || !this.data.trending) {
return;
}
// Define categories to show and their display info (excluding templates)
const categories = [
{ key: 'agents', title: 'Agents', icon: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z' },
{ key: 'commands', title: 'Commands', icon: 'M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM2.5 8a5.5 5.5 0 1 0 11 0 5.5 5.5 0 0 0-11 0z' },
{ key: 'mcps', title: 'MCPs', icon: 'M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c0 .026.009.051.025.072L2.5 7a.5.5 0 0 1 0 .708L1.025 9.133a.149.149 0 0 0-.025.072V10.5A1.5 1.5 0 0 0 2.5 12h2.793a.149.149 0 0 0 .072-.025L7 10.5a.5.5 0 0 1 .708 0l1.625 1.475c.021.016.046.025.072.025H12.5A1.5 1.5 0 0 0 14 10.5v-.793a.149.149 0 0 0-.025-.072L12.5 8a.5.5 0 0 1 0-.708l1.475-1.625a.149.149 0 0 0 .025-.072V4.5A1.5 1.5 0 0 0 12.5 3H9.707a.149.149 0 0 0-.072.025L8 4.5a.5.5 0 0 1-.708 0L5.867 3.025A.149.149 0 0 0 5.793 3H2.5z' },
{ key: 'settings', title: 'Settings', icon: 'M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z' },
{ key: 'hooks', title: 'Hooks', icon: 'M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4.2-4.2a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z' },
{ key: 'skills', title: 'Skills', icon: 'M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v8A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5zm1.886 6.914L15 7.151V12.5a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5V7.15l6.614 1.764a1.5 1.5 0 0 0 .772 0zM1.5 4h13a.5.5 0 0 1 .5.5v1.616L8.129 7.948a.5.5 0 0 1-.258 0L1 6.116V4.5a.5.5 0 0 1 .5-.5z' }
];
container.innerHTML = '';
categories.forEach(category => {
const categoryData = this.data.trending[category.key];
if (!categoryData || !Array.isArray(categoryData) || categoryData.length === 0) {
return;
}
// Sort by monthly downloads and take only the top 1
const topItem = categoryData
.sort((a, b) => (b.downloadsMonth || 0) - (a.downloadsMonth || 0))[0];
const itemElement = this.createPopularItemCard(category, topItem);
container.appendChild(itemElement);
});
}
renderTopCountries() {
const container = document.getElementById('top-countries-list');
if (!container || !this.data || !this.data.topCountries) {
return;
}
const topCountries = this.data.topCountries;
container.innerHTML = topCountries.map(country => `
<div class="country-item">
<div class="country-flag">${country.flag}</div>
<div class="country-info">
<div class="country-name">${country.name}</div>
<div class="country-stats">
<span class="country-downloads">${country.downloads.toLocaleString()}</span>
<span class="country-percentage">(${country.percentage}%)</span>
</div>
<div class="country-bar-container">
<div class="country-bar" style="width: ${country.percentage}%"></div>
</div>
</div>
</div>
`).join('');
}
createPopularItemCard(category, item) {
const itemElement = document.createElement('div');
itemElement.className = 'popular-item';
const totalDownloads = item.downloadsTotal || 0;
itemElement.innerHTML = `
<div class="popular-item-header">
<div class="popular-item-info">
<div class="popular-item-type">${category.title}</div>
<h4 class="popular-item-name">${item.name}</h4>
</div>
</div>
<div class="popular-total-downloads">
<span class="total-number">${totalDownloads.toLocaleString()}</span>
<span class="total-label">downloads</span>
</div>
<div class="popular-stats">
<button class="popular-install-btn" onclick="showInstallModal('${item.id || item.name}')">
<svg class="popular-install-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Get
</button>
</div>
`;
return itemElement;
}
renderChart() {
if (!this.data || !this.data.chartData) {
console.warn('No chart data available');
return;
}
const ctx = document.getElementById('downloadsChart');
if (!ctx) {
console.warn('Chart canvas not found');
return;
}
// Chart colors matching terminal theme (excluding templates)
const colors = {
commands: '#10b981', // emerald (green)
agents: '#f59e0b', // amber (yellow)
mcps: '#3b82f6', // blue
settings: '#8b5cf6', // violet
hooks: '#f97316', // orange
skills: '#ec4899' // pink
};
// Define the desired order for the legend (agents first - most popular, excluding templates)
const categoryOrder = ['agents', 'commands', 'mcps', 'settings', 'hooks', 'skills'];
// Prepare datasets in the specified order
const datasets = categoryOrder
.filter(category => this.data.chartData.series[category]) // Only include categories that exist
.map(category => ({
label: category.charAt(0).toUpperCase() + category.slice(1),
data: this.data.chartData.series[category],
borderColor: colors[category] || '#6b7280',
backgroundColor: (colors[category] || '#6b7280') + '20',
borderWidth: 2,
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 4,
pointBackgroundColor: colors[category] || '#6b7280',
pointBorderColor: '#1f2937',
pointBorderWidth: 2
}));
// Create chart
new Chart(ctx, {
type: 'line',
data: {
labels: this.data.chartData.dates.map(date => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#d1d5db',
usePointStyle: true,
pointStyle: 'circle',
padding: 20,
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 11
}
}
},
tooltip: {
backgroundColor: '#1f2937',
titleColor: '#f3f4f6',
bodyColor: '#d1d5db',
borderColor: '#374151',
borderWidth: 1,
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
filter: function(tooltipItem, data) {
return true; // Show all items
},
itemSort: function(a, b) {
// Sort tooltip items by download count (highest to lowest)
return b.parsed.y - a.parsed.y;
},
callbacks: {
title: function(context) {
return `Date: ${context[0].label}`;
},
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toLocaleString()} downloads`;
},
labelColor: function(context) {
// Use the same colors as defined for the chart lines
const category = context.dataset.label.toLowerCase();
const color = colors[category] || '#6b7280';
return {
borderColor: color,
backgroundColor: color
};
}
}
}
},
scales: {
x: {
grid: {
color: '#374151',
borderColor: '#4b5563'
},
ticks: {
color: '#9ca3af',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 10
}
}
},
y: {
grid: {
color: '#374151',
borderColor: '#4b5563'
},
ticks: {
color: '#9ca3af',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 10
},
callback: function(value) {
return value.toLocaleString();
}
}
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
renderTrendingItems() {
const container = document.getElementById('trending-list');
const items = this.getFilteredItems();
if (items.length === 0) {
container.innerHTML = this.getEmptyState();
return;
}
container.innerHTML = '';
items.forEach((item, index) => {
const itemElement = this.createTrendingItem(item, index + 1);
container.appendChild(itemElement);
});
}
getFilteredItems() {
let allItems = [];
if (this.currentType === '') {
// If no filter, get items from "all" category if it exists,
// otherwise combine all categories
if (this.data.trending.all) {
allItems = this.data.trending.all;
} else {
Object.values(this.data.trending).forEach(categoryItems => {
if (Array.isArray(categoryItems)) {
allItems = allItems.concat(categoryItems);
}
});
}
} else {
// Get items from selected category
allItems = this.data.trending[this.currentType] || [];
}
// Sort by the current date range downloads (highest to lowest)
allItems.sort((a, b) => {
const aDownloads = this.getDownloadsForRange(a);
const bDownloads = this.getDownloadsForRange(b);
return bDownloads - aDownloads;
});
// For "all" categories, limit to top 10
if (this.currentType === '') {
allItems = allItems.slice(0, 10);
}
return allItems;
}
createTrendingItem(item, rank) {
const itemElement = document.createElement('div');
itemElement.className = 'trending-item';
const category = item.category || 'general';
const itemType = this.getItemType(item);
const componentType = this.getComponentTypeLabel(item);
const downloads = this.getDownloadsForRange(item);
itemElement.innerHTML = `
<div class="trending-rank">
<span class="rank-number">#${rank}</span>
</div>
<div class="trending-content">
<div class="trending-header">
<div class="trending-title">
<svg class="trending-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="${this.getIconPath(itemType)}"/>
</svg>
<h3 class="trending-name">${item.name}</h3>
<span class="trending-category">${category}</span>
</div>
<button class="install-button" onclick="showInstallModal('${item.id || item.name}')">
<svg class="install-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Install
</button>
</div>
<div class="trending-metadata">
<div class="trending-downloads">
<svg class="meta-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
<span>${downloads.toLocaleString()} downloads ${this.getRangeLabel()}</span>
</div>
<div class="trending-type-badge">${componentType}</div>
</div>
</div>
`;
return itemElement;
}
getDownloadsForRange(item) {
switch(this.currentRange) {
case 'today':
return item.downloadsToday || 0;
case 'week':
return item.downloadsWeek || 0;
case 'month':
return item.downloadsMonth || 0;
default:
return item.downloadsToday || 0;
}
}
getRangeLabel() {
switch(this.currentRange) {
case 'today':
return 'today';
case 'week':
return 'this week';
case 'month':
return 'this month';
default:
return 'today';
}
}
getComponentTypeLabel(item) {
// Determine the component type from the item ID or context
const id = item.id || '';
if (id.includes('agent-') || id.startsWith('agent') || this.currentType === 'agents') {
return 'Agent';
} else if (id.includes('command-') || id.startsWith('command') || this.currentType === 'commands') {
return 'Command';
} else if (id.includes('setting-') || id.startsWith('setting') || this.currentType === 'settings') {
return 'Settings';
} else if (id.includes('hook-') || id.startsWith('hook') || this.currentType === 'hooks') {
return 'Hooks';
} else if (id.includes('mcp-') || id.startsWith('mcp') || this.currentType === 'mcps') {
return 'MCP';
} else if (id.includes('skill-') || id.startsWith('skill') || this.currentType === 'skills') {
return 'Skill';
} else if (id.includes('template-') || id.startsWith('template') || this.currentType === 'templates') {
return 'Template';
}
// Fallback: try to determine from data structure
if (item.expertise) return 'Agent';
if (item.command) return 'Command';
return 'Component';
}
renderContributors(contributors) {
if (!contributors || contributors.length === 0) {
return '';
}
const contributorElements = contributors.slice(0, 3).map(contributor =>
`<img class="contributor-avatar" src="${contributor.avatar}" alt="${contributor.name}" title="${contributor.name}">`
).join('');
return `
<div class="meta-item">
<span>Built by</span>
<div class="contributors">
${contributorElements}
</div>
</div>
`;
}
renderTags(tags) {
if (!tags || tags.length === 0) {
return '';
}
const tagElements = tags.map(tag =>
`<span class="tag">${tag}</span>`
).join('');
return `<div class="tags">${tagElements}</div>`;
}
getLanguageClass(language) {
const languageMap = {
'JavaScript/TypeScript': 'typescript',
'JavaScript': 'javascript',
'TypeScript': 'typescript',
'Node.js/Express': 'nodejs',
'Python': 'python',
'SQL/Node.js': 'sql',
'Docker': 'docker',
'C#/Unity': 'csharp',
'Unity/Game Development': 'unity',
'Git/Bash': 'git',
'Multi-language': 'javascript',
'Universal': 'javascript'
};
return languageMap[language] || 'javascript';
}
getItemType(item) {
// Determine item type based on data structure or properties
if (item.expertise) return 'agents';
if (item.command) return 'commands';
if (item.type === 'settings') return 'settings';
if (item.type === 'hooks') return 'hooks';
if (item.type === 'mcps') return 'mcps';
if (item.type === 'skills') return 'skills';
if (item.type === 'templates') return 'templates';
return 'commands'; // default
}
getIconPath(type) {
const icons = {
commands: 'M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM2.5 8a5.5 5.5 0 1 0 11 0 5.5 5.5 0 0 0-11 0z',
agents: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z',
settings: 'M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z',
hooks: 'M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4.2-4.2a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z',
mcps: 'M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c0 .026.009.051.025.072L2.5 7a.5.5 0 0 1 0 .708L1.025 9.133a.149.149 0 0 0-.025.072V10.5A1.5 1.5 0 0 0 2.5 12h2.793a.149.149 0 0 0 .072-.025L7 10.5a.5.5 0 0 1 .708 0l1.625 1.475c.021.016.046.025.072.025H12.5A1.5 1.5 0 0 0 14 10.5v-.793a.149.149 0 0 0-.025-.072L12.5 8a.5.5 0 0 1 0-.708l1.475-1.625a.149.149 0 0 0 .025-.072V4.5A1.5 1.5 0 0 0 12.5 3H9.707a.149.149 0 0 0-.072.025L8 4.5a.5.5 0 0 1-.708 0L5.867 3.025A.149.149 0 0 0 5.793 3H2.5z',
skills: 'M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v8A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5zm1.886 6.914L15 7.151V12.5a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5V7.15l6.614 1.764a1.5 1.5 0 0 0 .772 0zM1.5 4h13a.5.5 0 0 1 .5.5v1.616L8.129 7.948a.5.5 0 0 1-.258 0L1 6.116V4.5a.5.5 0 0 1 .5-.5z',
templates: 'M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H1.75zM7.25 4a.75.75 0 0 1 1.5 0v4.25H12a.75.75 0 0 1 0 1.5H8.75V12a.75.75 0 0 1-1.5 0V9.75H4a.75.75 0 0 1 0-1.5h3.25V4z'
};
return icons[type] || icons.commands;
}
getEmptyState() {
return `
<div class="empty-state">
<svg class="empty-state-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773l4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"/>
</svg>
<h3>No trending items found</h3>
<p>Try adjusting your filters or check back later for new trending content.</p>
</div>
`;
}
showError(message) {
const container = document.getElementById('trending-list');
container.innerHTML = `
<div class="empty-state">
<svg class="empty-state-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<h3>Error Loading Data</h3>
<p>${message}</p>
</div>
`;
}
showLoading() {
const container = document.getElementById('trending-list');
container.innerHTML = `
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading trending components...</p>
</div>
`;
}
}
// Initialize the trending page when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.trendingPageInstance = new TrendingPage();
});
// Modal functionality
function showInstallModal(componentName) {
const modal = document.getElementById('installModal');
const commandText = document.getElementById('commandText');
// Determine the component type from the current filter or component name
let componentType = 'command'; // default
let componentCategory = '';
// Get the current trending page instance to check the type
if (window.trendingPageInstance) {
componentType = window.trendingPageInstance.currentType || 'commands';
// Find the component in the data to get its category
const items = window.trendingPageInstance.data.trending[componentType] || [];
const item = items.find(i => (i.id || i.name) === componentName);
if (item && item.category) {
componentCategory = item.category;
}
}
// Convert plural to singular for the command flag
const typeMap = {
'agents': 'agent',
'commands': 'command',
'settings': 'setting',
'hooks': 'hook',
'mcps': 'mcp',
'skills': 'skill',
'templates': 'template'
};
const flagType = typeMap[componentType] || 'command';
// Clean the component name by removing prefixes
let cleanName = componentName;
const prefixesToRemove = ['agent-', 'command-', 'setting-', 'hook-', 'mcp-', 'skill-', 'template-'];
for (const prefix of prefixesToRemove) {
if (cleanName.startsWith(prefix)) {
cleanName = cleanName.substring(prefix.length);
break;
}
}
// Build the full component path: category/name
const componentPath = componentCategory ? `${componentCategory}/${cleanName}` : cleanName;
// Update the command with the correct flag and full component path
const command = `npx claude-code-templates@latest --${flagType} ${componentPath} --yes`;
commandText.textContent = command;
// Show the modal
modal.classList.add('show');
// Prevent body scrolling
document.body.style.overflow = 'hidden';
}
function closeInstallModal() {
const modal = document.getElementById('installModal');
modal.classList.remove('show');
// Restore body scrolling
document.body.style.overflow = '';
// Hide copy feedback
const feedback = document.getElementById('copyFeedback');
feedback.classList.remove('show');
}
function copyInstallCommand() {
const commandText = document.getElementById('commandText');
const copyFeedback = document.getElementById('copyFeedback');
const copyButton = document.querySelector('.copy-button');
// Copy to clipboard
navigator.clipboard.writeText(commandText.textContent).then(() => {
// Show success feedback
copyFeedback.classList.add('show');
// Temporarily change button text
const originalText = copyButton.innerHTML;
copyButton.innerHTML = `
<svg class="copy-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
Copied!
`;
// Reset after 2 seconds
setTimeout(() => {
copyButton.innerHTML = originalText;
copyFeedback.classList.remove('show');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
// Fallback for older browsers
fallbackCopyTextToClipboard(commandText.textContent);
});
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.width = "2em";
textArea.style.height = "2em";
textArea.style.padding = "0";
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
textArea.style.background = "transparent";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
const copyFeedback = document.getElementById('copyFeedback');
copyFeedback.classList.add('show');
setTimeout(() => {
copyFeedback.classList.remove('show');
}, 2000);
} catch (err) {
console.error('Fallback: Could not copy text: ', err);
}
document.body.removeChild(textArea);
}
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeInstallModal();
}
});
// Add some utility functions for future enhancements
window.TrendingUtils = {
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
},
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
};
+57
View File
@@ -0,0 +1,57 @@
// Shared Utility Functions
/**
* Escape HTML special characters to prevent XSS when injecting into innerHTML.
* Use this for any user-controlled or external data before inserting into HTML.
*/
function escapeHTML(str) {
if (typeof str !== 'string') return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function copyToClipboard(text, message = 'Command copied to clipboard!') {
navigator.clipboard.writeText(text).then(() => {
showNotification(message, 'success');
}).catch(err => {
console.error('Failed to copy: ', err);
showNotification('Failed to copy command', 'error');
});
}
function showNotification(message, type = 'info') {
// Remove any existing notifications first
const existingNotifications = document.querySelectorAll('.notification');
existingNotifications.forEach(notif => {
if (notif.parentNode) {
notif.parentNode.removeChild(notif);
}
});
const notification = document.createElement('div');
notification.textContent = message;
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
padding: 12px 24px;
border-radius: 6px;
z-index: 10000;
font-family: system-ui, -apple-system, sans-serif;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
File diff suppressed because it is too large Load Diff
+620
View File
@@ -0,0 +1,620 @@
// Workflow Builder JavaScript
// Global state
let workflowState = {
steps: [],
properties: {
name: '',
description: '',
tags: []
},
components: {
agents: [],
commands: [],
mcps: []
}
};
// GitHub API configuration
const GITHUB_CONFIG = {
owner: 'davila7',
repo: 'claude-code-templates',
branch: 'main'
};
// Initialize the workflow builder
document.addEventListener('DOMContentLoaded', async function() {
showLoadingSpinner();
try {
await loadAllComponents();
initializeDragAndDrop();
initializeEventListeners();
initializeModalEventListeners();
initializeSortableSteps();
} catch (error) {
console.error('Error initializing Workflow Builder:', error);
showError('Failed to load components. Please refresh the page.');
} finally {
hideLoadingSpinner();
}
});
// Load all components from GitHub using the Git Trees API
async function loadAllComponents() {
try {
const response = await fetch('components.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const allComponents = await response.json();
workflowState.components = {
agents: allComponents.agents.sort((a, b) => a.path.localeCompare(b.path)),
commands: allComponents.commands.sort((a, b) => a.path.localeCompare(b.path)),
mcps: allComponents.mcps.sort((a, b) => a.path.localeCompare(b.path))
};
renderComponentsList('agents', workflowState.components.agents);
renderComponentsList('commands', workflowState.components.commands);
renderComponentsList('mcps', workflowState.components.mcps);
} catch (error) {
console.error('Error loading components:', error);
loadComponentsWithFallback();
}
}
async function loadComponentsWithFallback() {
const [agents, commands, mcps] = [
getFallbackComponentData('agents'),
getFallbackComponentData('commands'),
getFallbackComponentData('mcps')
];
workflowState.components = { agents, commands, mcps };
renderComponentsList('agents', agents);
renderComponentsList('commands', commands);
renderComponentsList('mcps', mcps);
}
// Get fallback component data
function getFallbackComponentData(type) {
const fallbackData = {
agents: [
{ name: 'code-reviewer', path: 'development/code-reviewer', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'documentation-writer', path: 'development/documentation-writer', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'bug-hunter', path: 'development/bug-hunter', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'security-auditor', path: 'security/security-auditor', category: 'security', type: 'agent', icon: '🤖' },
{ name: 'performance-optimizer', path: 'optimization/performance-optimizer', category: 'optimization', type: 'agent', icon: '🤖' }
],
commands: [
{ name: 'git-setup', path: 'development/git-setup', category: 'development', type: 'command', icon: '⚡' },
{ name: 'project-init', path: 'development/project-init', category: 'development', type: 'command', icon: '⚡' },
{ name: 'docker-setup', path: 'devops/docker-setup', category: 'devops', type: 'command', icon: '⚡' },
{ name: 'test-runner', path: 'testing/test-runner', category: 'testing', type: 'command', icon: '⚡' },
{ name: 'build-pipeline', path: 'devops/build-pipeline', category: 'devops', type: 'command', icon: '⚡' }
],
mcps: [
{ name: 'database-connector', path: 'database/database-connector', category: 'database', type: 'mcp', icon: '🔌' },
{ name: 'api-client', path: 'api/api-client', category: 'api', type: 'mcp', icon: '🔌' },
{ name: 'file-manager', path: 'system/file-manager', category: 'system', type: 'mcp', icon: '🔌' },
{ name: 'redis-cache', path: 'database/redis-cache', category: 'database', type: 'mcp', icon: '🔌' },
{ name: 'email-service', path: 'communication/email-service', category: 'communication', type: 'mcp', icon: '🔌' }
]
};
const typeKey = type;
const data = fallbackData[typeKey] || [];
return data;
}
// Get icon for component type
function getComponentIcon(type) {
const icons = { 'agent': '🤖', 'command': '⚡', 'mcp': '🔌' };
return icons[type] || '📦';
}
// Render components list in the UI
function renderComponentsList(type, components) {
const container = document.getElementById(`${type}-tree`);
const countElement = document.getElementById(`${type}-count`);
if (!container || !countElement) return;
countElement.textContent = components.length;
container.innerHTML = '';
const groupedComponents = components.reduce((acc, component) => {
const category = component.category === 'root' ? 'general' : component.category;
if (!acc[category]) acc[category] = [];
acc[category].push(component);
return acc;
}, {});
Object.entries(groupedComponents).forEach(([category, categoryComponents]) => {
const folderElement = createTreeFolder(category, categoryComponents, type);
container.appendChild(folderElement);
});
}
// Create tree folder element
function createTreeFolder(category, components, type) {
const folderElement = document.createElement('div');
folderElement.className = 'tree-node';
const folderId = `${type}-${category.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`;
folderElement.innerHTML = `
<div class="tree-node-header" data-folder="${folderId}">
<span class="tree-icon folder-icon">📁</span>
<span class="tree-label">${category}</span>
<span class="tree-count">${components.length}</span>
<span class="tree-arrow">▶</span>
</div>
<div class="tree-children" id="${folderId}"></div>
`;
const childrenContainer = folderElement.querySelector('.tree-children');
components.forEach(component => {
const fileElement = createTreeFile(component);
childrenContainer.appendChild(fileElement);
});
return folderElement;
}
// Create tree file element
function createTreeFile(component) {
const element = document.createElement('div');
element.className = 'tree-file';
element.draggable = true;
element.dataset.componentType = component.type;
element.dataset.componentName = component.name;
element.dataset.componentPath = component.path;
element.dataset.componentCategory = component.category;
element.innerHTML = `
<div class="tree-file-header">
<span class="tree-icon file-icon">${getFileIcon(component.type)}</span>
<span class="tree-file-name">${component.name}</span>
<div class="tree-file-actions">
<button class="tree-action-btn" title="View Details" onclick="showComponentDetails('${component.type}', '${component.name}', '${component.path}', '${component.category}')">️</button>
<button class="tree-action-btn" title="Add to Workflow" onclick="addComponentFromButton(event)"></button>
</div>
</div>
`;
return element;
}
// Get file icon based on type
function getFileIcon(type) {
const icons = { 'agent': '📄', 'command': '⚡', 'mcp': '⚙️' };
return icons[type] || '📄';
}
// Initialize drag and drop
function initializeDragAndDrop() {
const workflowSteps = document.getElementById('workflowSteps');
document.addEventListener('dragstart', (event) => {
if (event.target.closest('.tree-file')) {
handleDragStart(event);
}
});
workflowSteps.addEventListener('dragover', handleDragOver);
workflowSteps.addEventListener('drop', handleDrop);
workflowSteps.addEventListener('dragenter', (e) => e.target.closest('.drop-zone')?.classList.add('drag-over'));
workflowSteps.addEventListener('dragleave', (e) => e.target.closest('.drop-zone')?.classList.remove('drag-over'));
}
function handleDragStart(event) {
const treeFile = event.target.closest('.tree-file');
const componentData = { ...treeFile.dataset };
event.dataTransfer.setData('application/json', JSON.stringify(componentData));
event.dataTransfer.effectAllowed = 'copy';
}
function handleDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}
function handleDrop(event) {
event.preventDefault();
event.target.closest('.drop-zone')?.classList.remove('drag-over');
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'));
addWorkflowStep(componentData);
} catch (error) {
console.error('Error handling drop:', error);
}
}
function addComponentFromButton(event) {
const treeFile = event.target.closest('.tree-file');
const componentData = { ...treeFile.dataset };
addWorkflowStep(componentData);
}
function addWorkflowStep(componentData) {
const step = {
id: `step_${Date.now()}`,
type: componentData.componentType,
name: componentData.componentName,
path: componentData.componentPath,
category: componentData.componentCategory,
description: `Execute ${componentData.componentName}`,
icon: getComponentIcon(componentData.componentType)
};
workflowState.steps.push(step);
renderWorkflowSteps();
updateWorkflowStats();
}
function renderWorkflowSteps() {
const container = document.getElementById('workflowSteps');
const dropZone = container.querySelector('.drop-zone');
const existingSteps = container.querySelectorAll('.workflow-step');
existingSteps.forEach(step => step.remove());
if (workflowState.steps.length > 0) {
dropZone.style.display = 'none';
workflowState.steps.forEach((step, index) => {
const stepElement = document.createElement('div');
stepElement.className = 'workflow-step';
stepElement.dataset.stepId = step.id;
stepElement.innerHTML = `
<div class="step-number">${index + 1}</div>
<div class="step-icon">${step.icon}</div>
<div class="step-content">
<div class="step-name">${step.name}</div>
<div class="step-type">${step.type}</div>
</div>
<div class="step-actions">
<button class="step-action details" onclick="showComponentDetails('${step.type}', '${step.name}', '${step.path}', '${step.category}')">️</button>
<button class="step-action remove" onclick="removeStep('${step.id}')">🗑️</button>
</div>
`;
container.appendChild(stepElement);
});
} else {
dropZone.style.display = 'flex';
}
initializeSortableSteps();
}
function initializeSortableSteps() {
const workflowSteps = document.getElementById('workflowSteps');
if (workflowState.steps.length > 0) {
new Sortable(workflowSteps, {
animation: 150,
onEnd: (evt) => {
const movedStep = workflowState.steps.splice(evt.oldIndex, 1)[0];
workflowState.steps.splice(evt.newIndex, 0, movedStep);
renderWorkflowSteps();
}
});
}
}
function removeStep(stepId) {
workflowState.steps = workflowState.steps.filter(step => step.id !== stepId);
renderWorkflowSteps();
updateWorkflowStats();
}
function updateWorkflowStats() {
const stats = { agents: 0, commands: 0, mcps: 0 };
workflowState.steps.forEach(step => stats[step.type + 's']++);
document.getElementById('agentCount').textContent = stats.agents;
document.getElementById('commandCount').textContent = stats.commands;
document.getElementById('mcpCount').textContent = stats.mcps;
document.getElementById('totalSteps').textContent = workflowState.steps.length;
}
function initializeEventListeners() {
document.getElementById('componentSearch').addEventListener('input', handleComponentSearch);
document.getElementById('clearCanvas').addEventListener('click', clearWorkflow);
document.getElementById('generateWorkflow').addEventListener('click', openPropertiesModal);
document.getElementById('closePropertiesModal').addEventListener('click', closePropertiesModal);
document.getElementById('saveWorkflowProperties').addEventListener('click', saveAndGenerateWorkflow);
// Category accordion toggle
document.querySelectorAll('.category-card-header').forEach(header => {
header.addEventListener('click', () => {
header.parentElement.classList.toggle('expanded');
});
});
// Add event listener for sub-folders
document.addEventListener('click', function(event) {
const header = event.target.closest('.tree-node-header');
if (header) {
const children = header.nextElementSibling;
if (children && children.classList.contains('tree-children')) {
header.classList.toggle('expanded');
children.classList.toggle('expanded');
}
}
});
}
function handleComponentSearch(event) {
const searchTerm = event.target.value.toLowerCase();
document.querySelectorAll('.tree-file').forEach(file => {
const match = file.dataset.componentName.toLowerCase().includes(searchTerm);
file.style.display = match ? '' : 'none';
});
}
function clearWorkflow() {
if (confirm('Are you sure you want to clear the workflow?')) {
workflowState.steps = [];
renderWorkflowSteps();
updateWorkflowStats();
}
}
function openPropertiesModal() {
if (workflowState.steps.length === 0) {
showError('Add at least one component to the workflow.');
return;
}
document.getElementById('propertiesModal').style.display = 'block';
}
function closePropertiesModal() {
document.getElementById('propertiesModal').style.display = 'none';
}
function saveAndGenerateWorkflow() {
workflowState.properties.name = document.getElementById('workflowName').value;
workflowState.properties.description = document.getElementById('workflowDescription').value;
workflowState.properties.tags = document.getElementById('workflowTags').value.split(',').map(t => t.trim());
if (!workflowState.properties.name) {
showError('Workflow name is required.');
return;
}
closePropertiesModal();
generateWorkflow();
}
async function generateWorkflow() {
showLoadingSpinner();
try {
const workflowHash = await generateWorkflowHash();
const yamlContent = generateWorkflowYAML();
showGenerateModal(workflowHash, yamlContent);
} catch (error) {
console.error('Error generating workflow:', error);
} finally {
hideLoadingSpinner();
}
}
async function generateWorkflowHash() {
const dataString = JSON.stringify(workflowState);
const hash = CryptoJS.SHA256(dataString).toString(CryptoJS.enc.Hex).substring(0, 12);
localStorage.setItem(`workflow_${hash}`, dataString);
return hash;
}
function generateWorkflowYAML() {
return `# Workflow: ${workflowState.properties.name}\n` +
`steps:\n` +
workflowState.steps.map((step, i) => ` - step: ${i+1}\n type: ${step.type}\n name: "${step.name}"`).join('\n');
}
function showGenerateModal(hash, yaml) {
document.getElementById('workflowCommand').textContent = `npx claude-code-templates@latest --workflow:#${hash}`;
document.getElementById('yamlContent').textContent = yaml;
document.getElementById('generateModal').style.display = 'block';
}
function showLoadingSpinner() { document.getElementById('loadingSpinner').style.display = 'flex'; }
function hideLoadingSpinner() { document.getElementById('loadingSpinner').style.display = 'none'; }
function showError(msg) { alert(msg); }
async function showComponentDetails(type, name, path, category) {
const component = workflowState.components[type + 's'].find(c => c.name === name);
if (!component) {
console.warn('Component not found:', type, name);
return;
}
showComponentModal(component);
}
// Show detailed component modal (recreated from original version)
function showComponentModal(component) {
const typeConfig = {
agent: { icon: '🤖', color: '#ff6b6b', label: 'AGENT' },
command: { icon: '⚡', color: '#4ecdc4', label: 'COMMAND' },
mcp: { icon: '🔌', color: '#45b7d1', label: 'MCP' }
};
const config = typeConfig[component.type];
const installCommand = generateInstallCommand(component);
const modalHTML = `
<div class="modal-overlay" onclick="closeComponentModal()">
<div class="modal-content component-modal" onclick="event.stopPropagation()">
<div class="modal-header">
<div class="component-modal-title">
<span class="component-icon" style="color: ${config.color}">${config.icon}</span>
<h3>${formatComponentName(component.name)}</h3>
<span class="component-type-badge" style="background: ${config.color}">${config.label}</span>
</div>
<button class="modal-close" onclick="closeComponentModal()">×</button>
</div>
<div class="modal-body">
<div class="component-details">
<div class="component-description">
${getComponentDescription(component)}
</div>
<div class="installation-section">
<h4>📦 Installation</h4>
<div class="command-line">
<code>${installCommand}</code>
<button class="copy-btn" onclick="copyToClipboard('${installCommand}')">Copy</button>
</div>
</div>
<div class="component-content">
<h4>📋 Component Details</h4>
<div class="component-preview">
<pre><code>${component.content ? component.content.substring(0, 500) + (component.content.length > 500 ? '...' : '') : 'No content available.'}</code></pre>
</div>
</div>
<div class="modal-actions">
<button class="github-folder-link" onclick="viewOnGitHub('${component.path}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.30.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</button>
</div>
</div>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.querySelector('.modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Helper functions for the modal
function formatComponentName(name) {
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
function generateInstallCommand(component) {
return `npx claude-code-templates@latest --${component.type} "${component.path}"`;
}
function getComponentDescription(component) {
if (!component.content) {
return 'No description available.';
}
// Try to extract description from frontmatter
const descMatch = component.content.match(/description:\s*(.+?)(?:\n|$)/);
if (descMatch) {
return descMatch[1].trim().replace(/^["']|["']$/g, '');
}
// Use first paragraph if no frontmatter description
const lines = component.content.split('\n');
const firstParagraph = lines.find(line => line.trim() && !line.startsWith('---') && !line.startsWith('#'));
if (firstParagraph) {
return firstParagraph.trim();
}
return 'No description available.';
}
function viewOnGitHub(path) {
const url = `https://github.com/davila7/claude-code-templates/tree/main/${path}`;
window.open(url, '_blank');
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showSuccess('Command copied to clipboard!');
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
function closeComponentModal() {
const modalOverlay = document.querySelector('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
function initializeModalEventListeners() {
// Component modal event listeners
document.getElementById('closeComponentModal').addEventListener('click', closeComponentModal);
document.getElementById('closeComponentModalBtn').addEventListener('click', closeComponentModal);
document.getElementById('addComponentToWorkflow').addEventListener('click', () => {
const componentData = JSON.parse(document.getElementById('componentModal').dataset.currentComponent);
addWorkflowStep({
componentType: componentData.type,
componentName: componentData.name,
componentPath: componentData.path,
componentCategory: componentData.category
});
closeComponentModal();
});
document.getElementById('copyUsageCommand').addEventListener('click', () => {
const command = document.getElementById('componentModalUsage').textContent;
navigator.clipboard.writeText(command).then(() => showSuccess('Command copied!'));
});
// Generate modal event listeners
const closeModal = document.getElementById('closeModal');
if (closeModal) {
closeModal.addEventListener('click', () => {
document.getElementById('generateModal').style.display = 'none';
});
}
const copyCommand = document.getElementById('copyCommand');
if (copyCommand) {
copyCommand.addEventListener('click', () => {
const command = document.getElementById('workflowCommand').textContent;
navigator.clipboard.writeText(command).then(() => showSuccess('Command copied!'));
});
}
const copyYaml = document.getElementById('copyYaml');
if (copyYaml) {
copyYaml.addEventListener('click', () => {
const yaml = document.getElementById('yamlContent').textContent;
navigator.clipboard.writeText(yaml).then(() => showSuccess('YAML copied!'));
});
}
// Close modals when clicking outside
window.addEventListener('click', (event) => {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
}
function showSuccess(message) {
// Simple success notification
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #10b981;
color: white;
padding: 12px 24px;
border-radius: 6px;
z-index: 10000;
font-family: system-ui, -apple-system, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 2000);
}
// Add functions to window object for inline event handlers
window.removeStep = removeStep;
window.showComponentDetails = showComponentDetails;
window.addComponentFromButton = addComponentFromButton;