// Index Events - Handles events specific to index.html // Global function to focus search input when clicking wrapper function focusSearchInput() { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.focus(); } } class IndexPageManager { constructor() { this.currentFilter = 'skills'; this.currentCategoryFilter = 'all'; this.currentSort = 'downloads'; // Default sort by downloads this.templatesData = null; this.componentsData = null; this.availableCategories = { agents: new Set(), commands: new Set(), mcps: new Set(), settings: new Set(), hooks: new Set(), skills: new Set(), templates: new Set(), plugins: new Set() }; // Pagination settings this.currentPage = 1; this.itemsPerPage = 24; // 3x3 grid this.totalPages = 1; // Framework icons mapping (from script.js) this.FRAMEWORK_ICONS = { // Languages 'common': 'devicon-gear-plain', 'javascript-typescript': 'devicon-javascript-plain', 'python': 'devicon-python-plain', 'ruby': 'devicon-ruby-plain', 'rust': 'devicon-rust-plain', 'go': 'devicon-go-plain', // JavaScript/TypeScript frameworks 'react': 'devicon-react-original', 'vue': 'devicon-vuejs-plain', 'angular': 'devicon-angularjs-plain', 'node': 'devicon-nodejs-plain', // Python frameworks 'django': 'devicon-django-plain', 'flask': 'devicon-flask-original', 'fastapi': 'devicon-fastapi-plain', // Ruby frameworks 'rails': 'devicon-rails-plain', 'sinatra': 'devicon-ruby-plain', // Default fallback 'default': 'devicon-devicon-plain' }; } async init() { try { // Setup event listeners first (they don't depend on data) this.setupEventListeners(); // Show loading state this.showLoadingState(true); // Load all components and templates at once await this.loadComponentsData(); await this.loadTemplatesData(); // Check if URL has a specific filter and update accordingly const filterFromURL = this.getFilterFromURL(); if (filterFromURL && filterFromURL !== this.currentFilter) { this.currentFilter = filterFromURL; // Update active filter chip document.querySelectorAll('.component-type-filters .filter-chip').forEach(btn => { btn.classList.remove('active'); }); const activeBtn = document.querySelector(`.component-type-filters [data-filter="${filterFromURL}"]`); if (activeBtn) { activeBtn.classList.add('active'); } } // Update sort selector for initial filter this.updateSortSelector(); // Display components this.displayCurrentFilter(); // Show category filters for the current filter if (typeof showCategoryFilters === 'function') { showCategoryFilters(this.currentFilter); } } catch (error) { console.error('Error initializing index page:', error); this.showError('Failed to load data. Please refresh the page.'); } finally { this.showLoadingState(false); } } // Get filter from URL path getFilterFromURL() { const path = window.location.pathname; const segments = path.split('/').filter(segment => segment); // Check if first segment is a valid filter const validFilters = ['agents', 'commands', 'settings', 'hooks', 'mcps', 'skills', 'templates', 'plugins']; const firstSegment = segments[0]; if (firstSegment && validFilters.includes(firstSegment)) { return firstSegment; } // Default to skills return 'skills'; } async loadTemplatesData() { try { // Templates are now loaded from components.json, not GitHub this.templatesData = await window.dataLoader.loadTemplates(); // Update display if templates were found if (this.templatesData && Object.keys(this.templatesData).length > 0) { this.displayCurrentFilter(); } } catch (error) { console.warn('Templates not available in components.json:', error); // Continue without templates - this is not critical } } async loadComponentsData() { try { // Load all components at once - the performance issue was mostly due to GitHub fetching // Now that we only use components.json, we can load all data safely this.componentsData = await window.dataLoader.loadAllComponents(); this.collectAvailableCategories(); } catch (error) { console.error('Error loading components:', error); // Use fallback data this.componentsData = window.dataLoader.getFallbackComponentData(); this.collectAvailableCategories(); } } // This method is no longer needed since we load all components at once // Kept for backward compatibility but does nothing async loadMoreComponentsInBackground() { // No-op: All components are now loaded in the initial request } // Check if data object is empty isDataEmpty(data) { return !data || ((!data.agents || data.agents.length === 0) && (!data.commands || data.commands.length === 0) && (!data.mcps || data.mcps.length === 0) && (!data.settings || data.settings.length === 0) && (!data.hooks || data.hooks.length === 0)); } // Show/hide loading state showLoadingState(isLoading) { const loadingElements = document.querySelectorAll('.loading-indicator, .loading-spinner'); const contentElements = document.querySelectorAll('#unifiedGrid, .filter-controls'); loadingElements.forEach(el => { el.style.display = isLoading ? 'flex' : 'none'; }); contentElements.forEach(el => { el.style.opacity = isLoading ? '0.7' : '1'; }); } setupEventListeners() { // Filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { const filter = e.target.dataset.filter; this.setFilter(filter); }); }); // Copy buttons document.addEventListener('click', (e) => { if (e.target.classList.contains('copy-btn')) { const command = e.target.previousElementSibling.textContent; this.copyToClipboard(command); } }); // Card flip functionality for template cards document.addEventListener('click', (e) => { const card = e.target.closest('.template-card'); // Prevent flipping for "add new" cards or if a button is clicked if (card && !card.classList.contains('add-template-card') && !e.target.closest('button')) { card.classList.toggle('flipped'); } }); // ESC key to close all flipped cards document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.template-card.flipped').forEach(card => { card.classList.remove('flipped'); }); } }); } setFilter(filter) { this.currentFilter = filter; this.currentPage = 1; // Reset to first page when changing filter this.currentCategoryFilter = 'all'; // Reset category filter to show all items // Update active button document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); // Update sort selector options based on filter this.updateSortSelector(); this.displayCurrentFilter(); // Show category filters for the new filter if (typeof showCategoryFilters === 'function') { showCategoryFilters(filter); } } // Update sort selector based on current filter updateSortSelector() { const sortSelector = document.getElementById('sortSelector'); if (!sortSelector) return; const currentValue = sortSelector.value; if (this.currentFilter === 'agents') { // Add "Verified" option for agents if it doesn't exist const verifiedOption = Array.from(sortSelector.options).find(opt => opt.value === 'verified'); if (!verifiedOption) { const option = document.createElement('option'); option.value = 'verified'; option.textContent = 'Verified First'; sortSelector.insertBefore(option, sortSelector.options[0]); } } else { // Remove "Verified" option for other filters const verifiedOption = Array.from(sortSelector.options).find(opt => opt.value === 'verified'); if (verifiedOption) { verifiedOption.remove(); // Reset to downloads if verified was selected if (currentValue === 'verified') { sortSelector.value = 'downloads'; this.currentSort = 'downloads'; } } } } // Set category filter setCategoryFilter(category) { this.currentCategoryFilter = category; this.currentPage = 1; // Reset to first page when changing category // Update category filter buttons document.querySelectorAll('.category-filter-btn').forEach(btn => { btn.classList.remove('active'); }); const targetBtn = document.querySelector(`[data-category="${category}"]`); if (targetBtn) { targetBtn.classList.add('active'); } // Regenerate the component display this.displayCurrentFilter(); } // Update category sub-filters in the unified-filter-bar updateCategorySubFilters() { const unifiedFilterBar = document.querySelector('.unified-filter-bar'); if (!unifiedFilterBar) return; // Remove existing category filters const existingCategoryFilters = unifiedFilterBar.querySelector('.category-filter-row'); if (existingCategoryFilters) { existingCategoryFilters.remove(); } // Get categories for current filter type const currentCategories = Array.from(this.availableCategories[this.currentFilter] || []).sort(); if (currentCategories.length <= 1) { // Don't show sub-filters if there's only one category or none return; } // Create category filter row const categoryFilterRow = document.createElement('div'); categoryFilterRow.className = 'category-filter-row'; categoryFilterRow.innerHTML = `
Categories:
${currentCategories.map(category => ` `).join('')}
`; // Add click event listeners categoryFilterRow.querySelectorAll('.category-filter-btn').forEach(btn => { btn.addEventListener('click', () => { this.setCategoryFilter(btn.getAttribute('data-category')); }); }); // Append to unified filter bar unifiedFilterBar.appendChild(categoryFilterRow); } // Get filtered components based on current filter and category filter getFilteredComponents(type) { if (!type) type = this.currentFilter; if (type === 'templates') { return []; } let components = this.componentsData[type] || []; // Apply category filter if not 'all' if (this.currentCategoryFilter !== 'all') { components = components.filter(component => { const category = component.category || 'general'; return category === this.currentCategoryFilter; }); } // Apply sorting components = this.sortComponents(components); return components; } // Sort components based on current sort option sortComponents(components) { const sortedComponents = [...components]; // Create a copy to avoid mutating original if (this.currentSort === 'downloads') { // Sort by downloads (descending) - components with no downloads go to the end sortedComponents.sort((a, b) => { const downloadsA = a.downloads || 0; const downloadsB = b.downloads || 0; return downloadsB - downloadsA; }); } else if (this.currentSort === 'alphabetical') { // Sort alphabetically by name sortedComponents.sort((a, b) => { const nameA = (a.name || '').toLowerCase(); const nameB = (b.name || '').toLowerCase(); return nameA.localeCompare(nameB); }); } else if (this.currentSort === 'verified') { // Sort by verified status first (100% score), then by downloads sortedComponents.sort((a, b) => { // Check if component has 100% validation score const aVerified = a.security && a.security.validated && a.security.score === 100 && a.security.valid; const bVerified = b.security && b.security.validated && b.security.score === 100 && b.security.valid; // Verified components come first if (aVerified && !bVerified) return -1; if (!aVerified && bVerified) return 1; // If both verified or both not verified, sort by downloads const downloadsA = a.downloads || 0; const downloadsB = b.downloads || 0; return downloadsB - downloadsA; }); } return sortedComponents; } // Handle sort change from the dropdown handleSortChange(sortValue) { this.currentSort = sortValue; this.currentPage = 1; // Reset to first page when changing sort this.displayCurrentFilter(); } // Collect available categories from loaded components collectAvailableCategories() { // Reset categories this.availableCategories.agents.clear(); this.availableCategories.commands.clear(); this.availableCategories.mcps.clear(); this.availableCategories.settings.clear(); this.availableCategories.hooks.clear(); this.availableCategories.skills.clear(); this.availableCategories.templates.clear(); // Collect categories from each component type if (this.componentsData.agents && Array.isArray(this.componentsData.agents)) { this.componentsData.agents.forEach(component => { const category = component.category || 'general'; this.availableCategories.agents.add(category); }); } if (this.componentsData.commands && Array.isArray(this.componentsData.commands)) { this.componentsData.commands.forEach(component => { const category = component.category || 'general'; this.availableCategories.commands.add(category); }); } if (this.componentsData.mcps && Array.isArray(this.componentsData.mcps)) { this.componentsData.mcps.forEach(component => { const category = component.category || 'general'; this.availableCategories.mcps.add(category); }); } if (this.componentsData.settings && Array.isArray(this.componentsData.settings)) { this.componentsData.settings.forEach(component => { const category = component.category || 'general'; this.availableCategories.settings.add(category); }); } if (this.componentsData.hooks && Array.isArray(this.componentsData.hooks)) { this.componentsData.hooks.forEach(component => { const category = component.category || 'general'; this.availableCategories.hooks.add(category); }); } if (this.componentsData.skills && Array.isArray(this.componentsData.skills)) { this.componentsData.skills.forEach(component => { const category = component.category || 'general'; this.availableCategories.skills.add(category); }); } // Collect categories from templates (use language as category for language templates) if (this.componentsData.templates && Array.isArray(this.componentsData.templates)) { this.componentsData.templates.forEach(template => { if (template.subtype === 'language') { this.availableCategories.templates.add(template.name); } else if (template.subtype === 'framework' && template.language) { this.availableCategories.templates.add(template.language); } }); } } displayCurrentFilter() { const grid = document.getElementById('unifiedGrid'); if (!grid) return; // Set proper grid class based on current filter if (this.currentFilter === 'templates') { grid.className = 'unified-grid templates-mode'; } else { grid.className = 'unified-grid components-mode'; } // Update filter button counts this.updateFilterCounts(); switch (this.currentFilter) { case 'templates': this.displayTemplates(grid); break; case 'plugins': this.displayPlugins(grid); break; case 'agents': case 'commands': case 'mcps': case 'settings': case 'hooks': case 'skills': this.displayComponents(grid, this.currentFilter); break; default: grid.innerHTML = '
Unknown filter
'; } } displayTemplates(grid) { if (!this.componentsData || !this.componentsData.templates) { grid.innerHTML = '
Loading templates...
'; return; } // Update category sub-filters for templates this.updateCategorySubFilters(); // Clear the grid grid.innerHTML = ''; // Add the "Add New Template" card first const addTemplateCard = this.createAddTemplateCard(); grid.appendChild(addTemplateCard); // Filter templates based on category selection let filteredTemplates = this.componentsData.templates; if (this.currentCategoryFilter !== 'all') { filteredTemplates = this.componentsData.templates.filter(template => { if (template.subtype === 'language') { return template.name === this.currentCategoryFilter; } else if (template.subtype === 'framework') { return template.language === this.currentCategoryFilter; } return false; }); } // Apply sorting to templates filteredTemplates = this.sortComponents(filteredTemplates); // Create template cards from the filtered list filteredTemplates.forEach(template => { const templateCard = this.createTemplateCardFromJSON(template); grid.appendChild(templateCard); }); } displayPlugins(grid) { if (!this.componentsData || !this.componentsData.plugins) { grid.innerHTML = '
Loading plugins...
'; return; } // Clear the grid grid.innerHTML = ''; // Get plugins from loaded components data const plugins = this.componentsData.plugins; if (plugins.length === 0) { grid.innerHTML = '
No plugins available
'; return; } // Add marketplace setup notice const marketplaceNotice = document.createElement('div'); marketplaceNotice.className = 'plugin-marketplace-notice'; marketplaceNotice.innerHTML = `
â„šī¸

First Time Setup Required

Before installing any plugin, you need to add the marketplace to Claude Code:

/plugin marketplace add https://github.com/davila7/claude-code-templates
`; grid.appendChild(marketplaceNotice); // Create plugin cards plugins.forEach(plugin => { const pluginCard = this.createPluginCard(plugin); grid.appendChild(pluginCard); }); } createPluginCard(plugin) { const card = document.createElement('div'); card.className = 'template-card plugin-card'; // Count components const totalComponents = (plugin.commands || 0) + (plugin.agents || 0) + (plugin.mcpServers || 0); card.innerHTML = `
v${plugin.version}

${escapeHTML(this.formatComponentName(plugin.name))}

${escapeHTML(plugin.description)}

${plugin.commands > 0 ? `⚡${plugin.commands}` : ''} ${plugin.agents > 0 ? `🤖${plugin.agents}` : ''} ${plugin.mcpServers > 0 ? `🔌${plugin.mcpServers}` : ''}

Installation Command

${plugin.installCommand}

Included Components (${totalComponents})

${plugin.commands > 0 ? `
⚡ ${plugin.commands} Command${plugin.commands > 1 ? 's' : ''}
` : ''} ${plugin.agents > 0 ? `
🤖 ${plugin.agents} Agent${plugin.agents > 1 ? 's' : ''}
` : ''} ${plugin.mcpServers > 0 ? `
🔌 ${plugin.mcpServers} MCP${plugin.mcpServers > 1 ? 's' : ''}
` : ''}
`; return card; } displayComponents(grid, type) { if (!this.componentsData) { grid.innerHTML = '
Loading components...
'; return; } // Update category sub-filters in the unified-filter-bar this.updateCategorySubFilters(); const allComponents = this.getFilteredComponents(type); // Calculate pagination const totalItems = allComponents.length + 1; // +1 for "Add New" card this.totalPages = Math.ceil(totalItems / this.itemsPerPage); // Get components for current page const startIndex = (this.currentPage - 1) * this.itemsPerPage; const endIndex = startIndex + this.itemsPerPage; let html = ''; let itemsToShow = []; // Add "Add New" card to the beginning itemsToShow.push({ type: 'add-new', data: type }); // Add components allComponents.forEach(component => { itemsToShow.push({ type: 'component', data: component }); }); // Get items for current page const pageItems = itemsToShow.slice(startIndex, endIndex); // Generate HTML for page items pageItems.forEach(item => { if (item.type === 'add-new') { html += this.createAddComponentCard(item.data); } else { html += this.generateComponentCard(item.data); } }); // Create pagination controls const paginationHTML = this.createPaginationControls(); grid.innerHTML = html || '
No components available
'; // Add pagination after the grid this.updatePagination(paginationHTML); } generateComponentCard(component) { // Generate install command - remove .md extension from path let componentPath = component.path || component.name; // Remove .md or .json extensions from path if (componentPath.endsWith('.md') || componentPath.endsWith('.json')) { componentPath = componentPath.replace(/\.(md|json)$/, ''); } if (componentPath.endsWith('.json')) { componentPath = componentPath.replace(/\.json$/, ''); } const installCommand = `npx claude-code-templates@latest --${component.type}=${componentPath} --yes`; const typeConfig = { agent: { icon: '🤖', color: '#ff6b6b' }, command: { icon: '⚡', color: '#4ecdc4' }, mcp: { icon: '🔌', color: '#45b7d1' }, setting: { icon: 'âš™ī¸', color: '#9c88ff' }, hook: { icon: 'đŸĒ', color: '#ff8c42' }, skill: { icon: '🎨', color: '#f59e0b' } }; const config = typeConfig[component.type]; // Escape quotes and special characters for onclick attributes const escapedType = component.type.replace(/'/g, "\\'"); const escapedName = (component.name || '').replace(/'/g, "\\'"); const escapedPath = (component.path || component.name || '').replace(/'/g, "\\'"); const escapedCategory = (component.category || 'general').replace(/'/g, "\\'"); const escapedCommand = installCommand.replace(/'/g, "\\'"); // Create category label (use "General" if no category) const categoryName = component.category || 'general'; const categoryLabel = `
${escapeHTML(this.formatComponentName(categoryName))}
`; // Create download badge if downloads data exists const downloadBadge = component.downloads && component.downloads > 0 ? `
${this.formatNumber(component.downloads)}
` : ''; // Create validation badge for agents with security data const validationBadge = this.createValidationBadge(component.security); return `
${downloadBadge} ${categoryLabel}

${escapeHTML(this.formatComponentName(component.name))}

${component.type === 'mcp' ? `

${escapeHTML(this.truncateDescription(component.description || 'MCP integration for enhanced development workflow', 80))}

` : `

${escapeHTML(this.getComponentDescription(component))}

` }

Installation Command

${escapeHTML(installCommand)}
`; } copyToClipboard(text) { // Use the global function from utils.js if (typeof window.copyToClipboard === 'function') { window.copyToClipboard(text); } else { copyToClipboard(text); } } showNotification(message, type = 'info') { 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); } formatComponentName(name) { return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); } formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } truncateDescription(description, maxLength = 80) { if (!description) return ''; if (description.length <= maxLength) return description; return description.substring(0, maxLength).trim() + '...'; } getComponentDescription(component) { let description = ''; if (component.description) { description = component.description; } else if (component.content) { // Try to extract description from frontmatter const descMatch = component.content.match(/description:\s*(.+?)(?:\n|$)/); if (descMatch) { description = descMatch[1].trim().replace(/^["']|["']$/g, ''); } else { // 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) { description = firstParagraph.trim(); } } } if (!description) { description = `A ${component.type} component for Claude Code.`; } // Truncate description to max 120 characters for proper card display if (description.length > 120) { description = description.substring(0, 117) + '...'; } return description; } /** * Create validation badge HTML - Only for perfect 100% score */ createValidationBadge(validation) { if (!validation || !validation.validated) return ''; const score = validation.score || 0; const isValid = validation.valid; // ONLY show badge for perfect score (100%) if (score === 100 && isValid) { return `
`; } // Don't show anything for scores below 100 return ''; } // Update filter button counts updateFilterCounts() { // Get accurate total counts from data loader (includes full data counts) const totalCounts = window.dataLoader.getTotalCounts(); if (!totalCounts) return; // Update each filter button with accurate total count const agentsBtn = document.querySelector('[data-filter="agents"]'); const commandsBtn = document.querySelector('[data-filter="commands"]'); const mcpsBtn = document.querySelector('[data-filter="mcps"]'); const settingsBtn = document.querySelector('[data-filter="settings"]'); const hooksBtn = document.querySelector('[data-filter="hooks"]'); const skillsBtn = document.querySelector('[data-filter="skills"]'); const templatesBtn = document.querySelector('[data-filter="templates"]'); if (agentsBtn) { agentsBtn.innerHTML = `🤖agents (${totalCounts.agents})`; } if (commandsBtn) { commandsBtn.innerHTML = `⚡commands (${totalCounts.commands})`; } if (mcpsBtn) { mcpsBtn.innerHTML = `🔌mcps (${totalCounts.mcps})`; } if (settingsBtn) { settingsBtn.innerHTML = `âš™ī¸settings (${totalCounts.settings})`; } if (hooksBtn) { hooksBtn.innerHTML = `đŸĒhooks (${totalCounts.hooks})`; } if (skillsBtn) { skillsBtn.innerHTML = `NEW🎨skills (${totalCounts.skills})`; } if (templatesBtn) { templatesBtn.innerHTML = `đŸ“Ļtemplates (${totalCounts.templates})`; } } // Create Add Component card createAddComponentCard(type) { const typeConfig = { agents: { icon: '🤖', name: 'Agent', description: 'Create a new AI specialist agent', color: '#ff6b6b' }, commands: { icon: '⚡', name: 'Command', description: 'Add a custom slash command', color: '#4ecdc4' }, mcps: { icon: '🔌', name: 'MCP', description: 'Build a Model Context Protocol integration', color: '#45b7d1' }, settings: { icon: 'âš™ī¸', name: 'Setting', description: 'Configure Claude Code behavior', color: '#9c88ff' }, hooks: { icon: 'đŸĒ', name: 'Hook', description: 'Automate tool execution workflows', color: '#ff8c42' }, skills: { icon: '🎨', name: 'Skill', description: 'Add modular capabilities with progressive disclosure', color: '#f59e0b' } }; const config = typeConfig[type]; if (!config) return ''; return `

Add New ${config.name}

${config.description}

`; } showError(message) { const grid = document.getElementById('unifiedGrid'); if (grid) { grid.innerHTML = `

Error

${message}

`; } } // Get framework icon from mapping getFrameworkIcon(framework) { return this.FRAMEWORK_ICONS[framework] || this.FRAMEWORK_ICONS['default']; } // Create Add Template card createAddTemplateCard() { const card = document.createElement('div'); card.className = 'template-card add-template-card'; card.innerHTML = `

Add New Template

Contribute a new language or framework to the community

`; // Add click handler card.addEventListener('click', () => { showComponentContributeModal('templates'); }); return card; } // Create template card from JSON structure createTemplateCardFromJSON(template) { const card = document.createElement('div'); card.className = 'template-card'; // Determine the icon based on template name/type const icon = this.getFrameworkIcon(template.name); // Create the display name const displayName = template.subtype === 'framework' ? `${template.language}/${template.name}` : template.name; card.innerHTML = `

${escapeHTML(displayName)}

${escapeHTML(template.description)}

Installation Command

${escapeHTML(template.installCommand)}
`; // Card flip is handled by global event listener in setupEventListeners return card; } // Create individual template card (legacy method, keep for compatibility) createTemplateCard(languageKey, languageData, frameworkKey, frameworkData) { const card = document.createElement('div'); card.className = `template-card ${languageData.comingSoon ? 'coming-soon' : ''}`; const displayName = frameworkKey === 'none' ? frameworkData.name : `${languageData.name.split('/')[0]}/${frameworkData.name}`; card.innerHTML = `
${languageData.comingSoon ? '
Coming Soon
' : ''}

${escapeHTML(displayName)}

${escapeHTML((languageData.description || '').substring(0, 120))}${(languageData.description || '').length > 120 ? '...' : ''}

Installation Options

${frameworkData.command}
`; // Add click handler for card flip (only if not coming soon) if (!languageData.comingSoon) { card.addEventListener('click', (e) => { // Don't flip if clicking on buttons if (!e.target.closest('button')) { card.classList.toggle('flipped'); } }); } return card; } // Fetch templates configuration from GitHub async fetchTemplatesConfig() { const GITHUB_CONFIG = { owner: 'davila7', repo: 'claude-code-templates', branch: 'main', templatesPath: 'cli-tool/src/templates.js' }; try { const url = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}?t=${Date.now()}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const templateFileContent = await response.text(); this.templatesData = this.parseTemplatesConfig(templateFileContent); return this.templatesData; } catch (error) { console.error('Error fetching templates:', error); throw error; } } // Parse templates configuration 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'); return JSON.parse(configString); } catch (error) { console.error('Error parsing templates config:', error); return null; } } // Show contribute modal showContributeModal() { alert('Contribute modal would open here - this needs to be implemented with the full modal HTML from script.js'); } // Create pagination controls createPaginationControls() { if (this.totalPages <= 1) return ''; let paginationHTML = '
'; paginationHTML += ''; paginationHTML += `
Page ${this.currentPage} of ${this.totalPages}
`; paginationHTML += '
'; return paginationHTML; } // Navigate to specific page goToPage(page) { if (page < 1 || page > this.totalPages || page === this.currentPage) return; this.currentPage = page; this.displayCurrentFilter(); // Scroll to top of content grid const contentGrid = document.getElementById('contentGrid'); if (contentGrid) { contentGrid.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } // Update pagination controls updatePagination(paginationHTML) { // Remove any existing pagination containers from the entire document const existingPaginations = document.querySelectorAll('.pagination-container'); existingPaginations.forEach(pagination => pagination.remove()); // Add new pagination if needed if (this.totalPages > 1 && paginationHTML) { const contentGrid = document.getElementById('contentGrid'); if (contentGrid) { contentGrid.insertAdjacentHTML('afterend', paginationHTML); } } } } // Global function for component details is now handled by modal-helpers.js // Global function for copying is now handled by utils.js // Global function for handling sort change (called from onchange) function handleSortChange(sortValue) { if (window.indexManager) { window.indexManager.handleSortChange(sortValue); } } // Global function for handling filter click with navigation function handleFilterClick(event, filter) { event.preventDefault(); // Prevent default link navigation // If plugins filter, scroll to plugins section if (filter === 'plugins') { const contentGrid = document.getElementById('contentGrid'); if (contentGrid) { contentGrid.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } setUnifiedFilter(filter); } // Global function for setting filter (called from onclick) function setUnifiedFilter(filter) { if (window.indexManager) { window.indexManager.setFilter(filter); } // Update filter buttons - remove active from ALL filter buttons document.querySelectorAll('.component-type-filters .filter-chip').forEach(btn => { btn.classList.remove('active'); }); // Add active class only to the clicked filter button const activeBtn = document.querySelector(`.component-type-filters [data-filter="${filter}"]`); if (activeBtn) { activeBtn.classList.add('active'); } // Update URL with filter parameter if (typeof updateURLWithFilter === 'function') { updateURLWithFilter(filter); } // Show category filters for the selected component type showCategoryFilters(filter); } // Function to show category filters based on component type function showCategoryFilters(componentType) { const categoryContainer = document.getElementById('componentCategories'); const categoryChips = document.getElementById('categoryChips'); if (!categoryContainer || !categoryChips) return; // Get categories from actual component data let categories = []; if (window.dataLoader) { const dataLoader = window.dataLoader; switch(componentType) { case 'agents': const agents = dataLoader.getComponentsByType('agent'); categories = getUniqueCategories(agents); break; case 'commands': const commands = dataLoader.getComponentsByType('command'); categories = getUniqueCategories(commands); break; case 'settings': try { categories = dataLoader.getSettingCategories ? dataLoader.getSettingCategories() : getUniqueCategories(dataLoader.getSettings()); } catch (e) { const settings = dataLoader.getComponentsByType('setting') || dataLoader.getSettings(); categories = getUniqueCategories(settings); } break; case 'hooks': try { categories = dataLoader.getHookCategories ? dataLoader.getHookCategories() : getUniqueCategories(dataLoader.getHooks()); } catch (e) { const hooks = dataLoader.getComponentsByType('hook') || dataLoader.getHooks(); categories = getUniqueCategories(hooks); } break; case 'mcps': const mcps = dataLoader.getComponentsByType('mcp'); categories = getUniqueCategories(mcps); break; case 'skills': const skills = dataLoader.getComponentsByType('skill'); categories = getUniqueCategories(skills); break; case 'templates': const templates = dataLoader.getComponentsByType('template'); categories = getUniqueCategories(templates); break; case 'plugins': // Plugins don't have categories categories = []; break; default: categories = []; } } // Add "All" option at the beginning if (categories.length > 0) { categories.unshift('All'); } if (categories.length > 0) { categoryChips.innerHTML = categories.map((category, index) => { const displayName = escapeHTML(formatCategoryName(category)); const safeCategory = escapeHTML(category.toLowerCase()); // Only the first item (All) should be active by default const isActive = index === 0 ? 'active' : ''; return ` `; }).join(''); categoryContainer.style.display = 'block'; } else { categoryContainer.style.display = 'none'; } } // Helper function to extract unique categories from component data function getUniqueCategories(components) { if (!components || !Array.isArray(components)) return []; const categories = new Set(); components.forEach(component => { if (component.category && component.category.trim() !== '') { categories.add(component.category); } }); return Array.from(categories).sort(); } // Helper function to format category names for display function formatCategoryName(category) { if (category === 'All') return 'All'; // Convert category names to proper case return category .split(/[-_\s]+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); } // Function to handle category filter selection // Enhanced setCategoryFilter function that handles both old and new category systems window.setCategoryFilter = function setCategoryFilter(category) { // Handle the new category chips in #categoryChips const categoryButtons = document.querySelectorAll('#categoryChips button.filter-chip'); if (categoryButtons.length > 0) { categoryButtons.forEach((btn) => { btn.classList.remove('active'); }); // Add active class only to the clicked button const activeBtn = document.querySelector(`#categoryChips button[data-category="${category}"]`); if (activeBtn) { activeBtn.classList.add('active'); } } // Also call the existing IndexManager setCategoryFilter method for actual filtering if (window.indexManager && window.indexManager.setCategoryFilter) { window.indexManager.setCategoryFilter(category); } // Force re-render of the current filter to apply category filtering if (window.indexManager && window.indexManager.displayCurrentFilter) { window.indexManager.displayCurrentFilter(); } } // Global helper functions for template cards function showInstallationFiles(languageKey, frameworkKey, displayName) { alert(`Installation files for ${displayName} would be shown here.`); } // Global function for template details function showTemplateDetails(templateId, templateName, subtype) { if (!window.indexManager || !window.indexManager.componentsData) { console.error('IndexManager or components data not available'); return; } // Find the template in the data const template = window.indexManager.componentsData.templates.find(t => t.id === templateId); if (!template) { console.error('Template not found:', templateId); return; } // Create modal to show template files const modalHTML = ` `; // Remove existing modal if present const existingModal = document.querySelector('.modal-overlay'); if (existingModal) { existingModal.remove(); } // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHTML); } // Global function for setting category filter (called from onclick) // setCategoryFilter function is now defined globally above as window.setCategoryFilter // Show component contribute modal (copied from script.js) function showComponentContributeModal(type) { const typeConfig = { agents: { name: 'Agent', description: 'AI specialist that handles specific development tasks', example: 'python-testing-specialist', structure: '- Agent metadata (name, description, color)\n- Core expertise areas\n- When to use guidelines\n- Code examples and patterns' }, commands: { name: 'Command', description: 'Custom slash command for Claude Code', example: 'optimize-bundle', structure: '- Command description and usage\n- Task breakdown\n- Process steps\n- Best practices and examples' }, mcps: { name: 'MCP', description: 'Model Context Protocol integration', example: 'redis-integration', structure: '- MCP server configuration\n- Connection parameters\n- Environment variables\n- Usage examples' }, settings: { name: 'Setting', description: 'Claude Code configuration setting', example: 'custom-model-config', structure: '- Setting description\n- Configuration options\n- Environment variables\n- Usage examples and best practices' }, hooks: { name: 'Hook', description: 'Automation hook for tool execution', example: 'format-on-save', structure: '- Hook description and trigger\n- Command to execute\n- PreToolUse or PostToolUse configuration\n- Error handling and examples' }, templates: { name: 'Template', description: 'Project template with language or framework setup', example: 'python or django-app', structure: 'For Languages: Create folder with base files\nFor Frameworks: Add to examples/ subfolder with specific setup' }, skills: { name: 'Skill', description: 'Interactive skill for specialized tasks', example: 'data-visualization-skill', structure: '- Skill description and purpose\n- Interactive prompts and parameters\n- Input/output specifications\n- Usage examples and best practices' } }; const config = typeConfig[type]; let modalHTML = ''; if (type === 'templates') { // Special modal for templates modalHTML = ` `; } else { // Default modal for other component types modalHTML = ` `; } // Remove existing modal if present const existingModal = document.querySelector('.modal-overlay'); if (existingModal) { existingModal.remove(); } // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHTML); // Add event listener for ESC key const handleEscape = (e) => { if (e.key === 'Escape') { closeComponentModal(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); } // Global functions for templates functionality function showInstallationFiles(languageKey, frameworkKey, displayName) { alert(`Installation files for ${displayName} would be shown here`); } // Close modal (from script.js) function closeModal() { const modal = document.querySelector('.modal'); if (modal) { modal.remove(); } } // Close component modal function closeComponentModal() { const modalOverlay = document.querySelector('.modal-overlay'); if (modalOverlay) { modalOverlay.remove(); } } // Global pagination function (called from onclick) function goToPage(page) { if (window.indexManager) { window.indexManager.goToPage(page); } } // Clean component name by removing extensions and formatting function 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; } // Handle Add to Cart button click function handleAddToCart(name, path, type, category, buttonElement) { // Prevent event propagation to avoid card flip if (window.event) { window.event.stopPropagation(); } // Clean the component name for better display const cleanName = getCleanComponentName(name); const item = { name: cleanName, path: path, category: category, description: `${cleanName} - ${category}` }; // Add to cart using the cart manager const success = addToCart(item, type); if (success) { // Update button state buttonElement.classList.add('added'); buttonElement.innerHTML = ` Added to Stack `; // Show a brief animation buttonElement.style.transform = 'scale(0.95)'; setTimeout(() => { buttonElement.style.transform = 'scale(1)'; }, 150); } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.indexManager = new IndexPageManager(); window.indexManager.init(); // Focus search input on page load and setup terminal cursor setTimeout(() => { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.focus(); setupTerminalCursor(); } }, 300); // Small delay to ensure DOM is fully loaded }); // Terminal cursor functionality function setupTerminalCursor() { const searchInput = document.getElementById('searchInput'); const cursor = document.getElementById('terminalCursor'); if (!searchInput || !cursor) return; function updateCursorPosition() { const promptWidth = 26; // Width of ">" prompt + extra space // If input has text, position cursor at the end of the text if (searchInput.value.length > 0) { // Create a temporary span to measure text width const temp = document.createElement('span'); temp.style.visibility = 'hidden'; temp.style.position = 'absolute'; temp.style.whiteSpace = 'pre'; temp.style.font = window.getComputedStyle(searchInput).font; temp.textContent = searchInput.value; document.body.appendChild(temp); const textWidth = temp.getBoundingClientRect().width; document.body.removeChild(temp); cursor.style.left = `${promptWidth + textWidth + 2}px`; } else { // If input is empty, position cursor right after the prompt with space cursor.style.left = `${promptWidth}px`; } } // Update cursor position on input searchInput.addEventListener('input', updateCursorPosition); searchInput.addEventListener('focus', () => { cursor.style.display = 'block'; updateCursorPosition(); }); searchInput.addEventListener('blur', () => { cursor.style.display = 'none'; }); // Initial position updateCursorPosition(); }