chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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, '"')}" 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, '"')}" 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()} "your prompt here"" 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 "your development task"" 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
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(/"([^&]+?)"/g, '<span style="color: #ce9178;">"$1"</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()}`;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
@@ -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;
|
||||
Reference in New Issue
Block a user