// Component Page JavaScript class ComponentPageManager { constructor() { this.component = null; this.dataLoader = null; this.init(); } async init() { // Initialize data loader this.dataLoader = window.dataLoader || new DataLoader(); // Load component from URL parameters await this.loadComponentFromURL(); // Setup event listeners this.setupEventListeners(); } async loadComponentFromURL() { let componentType, componentName, componentPath; // Check if we're using SEO-friendly URLs: /component/type/name const pathParts = window.location.pathname.split('/').filter(part => part); if (pathParts.length >= 3 && pathParts[0] === 'component') { componentType = decodeURIComponent(pathParts[1]); componentName = decodeURIComponent(pathParts[2]); } else { // Fallback to query parameters const urlParams = new URLSearchParams(window.location.search); componentType = urlParams.get('type'); componentName = urlParams.get('name'); componentPath = urlParams.get('path'); } if (!componentType || (!componentName && !componentPath)) { this.showError('Missing component parameters in URL'); return; } try { // Load all components data const componentsData = await this.dataLoader.loadAllComponents(); if (!componentsData) { throw new Error('Failed to load components data'); } // Find the component const categoryKey = componentType + 's'; // Convert 'agent' to 'agents' const components = componentsData[categoryKey] || []; let component = null; // Try to find by name first, then by path if (componentName) { component = components.find(c => c.name === componentName); // If not found, try to find by path that ends with the component name if (!component) { component = components.find(c => { // Check if path ends with componentName.md or componentName.json const pathEnd = `${componentName}.md`; const pathEndJson = `${componentName}.json`; return c.path && (c.path.endsWith(pathEnd) || c.path.endsWith(pathEndJson)); }); } } if (!component && componentPath) { component = components.find(c => c.path === componentPath); } if (!component) { throw new Error(`Component not found: ${componentType}/${componentName || componentPath}`); } this.component = { ...component, type: componentType }; await this.renderComponent(); } catch (error) { console.error('Error loading component:', error); this.showError(error.message); } } async renderComponent() { if (!this.component) { this.showError('No component data available'); return; } // Hide loading state document.getElementById('loadingState').style.display = 'none'; // Show component content document.getElementById('componentContent').style.display = 'block'; // Render component header this.renderComponentHeader(); // Render component description this.renderComponentDescription(); // Render security validation section this.renderSecurityValidation(); // Render metadata section await this.renderMetadataSection(); // Render installation section this.renderInstallationSection(); // Render component code await this.renderComponentCode(); // Render GitHub link this.renderGitHubLink(); // Populate sidebar quick install this.renderSidebarQuickInstall(); // Update page metadata this.updatePageMetadata(); // Load Giscus comments this.loadGiscusComments(); // Activate tab from URL hash this.activateTabFromHash(); // Track component view window.eventTracker?.track('component_view', { component_type: this.component.type, component_name: this.component.name, category: this.component.category || null }); } renderComponentHeader() { const typeConfig = { agent: { icon: '🤖', color: '#ff6b6b', badge: 'AGENT' }, command: { icon: '⚡', color: '#4ecdc4', badge: 'COMMAND' }, mcp: { icon: '🔌', color: '#45b7d1', badge: 'MCP' }, setting: { icon: '⚙️', color: '#9c88ff', badge: 'SETTING' }, hook: { icon: '🪝', color: '#ff8c42', badge: 'HOOK' }, template: { icon: '📦', color: '#f9a825', badge: 'TEMPLATE' } }; const config = typeConfig[this.component.type] || typeConfig.template; const formattedName = this.formatComponentName(this.component.name); // Update icon with null check const iconElement = document.getElementById('componentIcon'); if (iconElement) { iconElement.textContent = config.icon; } // Update title with null check const titleElement = document.getElementById('componentTitle'); if (titleElement) { titleElement.textContent = formattedName; } // Update type badge with null check const typeBadge = document.getElementById('componentTypeBadge'); if (typeBadge) { typeBadge.textContent = config.badge; typeBadge.style.backgroundColor = config.color; } // Update category - restored for original design const category = this.component.category || 'General'; const categoryElement = document.getElementById('componentCategory'); if (categoryElement) { categoryElement.textContent = category.charAt(0).toUpperCase() + category.slice(1); } // Update download badge const downloadBadge = document.getElementById('componentDownloadBadge'); const downloadCount = document.getElementById('downloadCount'); if (downloadBadge && downloadCount) { const downloads = this.component.downloads || 0; if (downloads > 0) { downloadBadge.style.display = 'inline-flex'; downloadCount.textContent = this.formatNumber(downloads); downloadBadge.title = `${downloads.toLocaleString()} downloads`; } else { downloadBadge.style.display = 'none'; } } // Add to cart button is set up in setupEventListeners() } renderComponentDescription() { const description = this.getComponentDescription(); const descriptionElement = document.getElementById('componentDescription'); if (descriptionElement) { descriptionElement.textContent = description; } } renderSecurityValidation() { const security = this.component.security; // If no security data, hide everything if (!security || !security.validated) { const validationSection = document.getElementById('validationSection'); if (validationSection) validationSection.style.display = 'none'; const validationBadge = document.getElementById('componentValidationBadge'); if (validationBadge) validationBadge.style.display = 'none'; return; } // Show validation section const validationSection = document.getElementById('validationSection'); if (validationSection) validationSection.style.display = 'block'; const score = security.score || 0; const isValid = security.valid; const errors = security.errorCount || 0; const warnings = security.warningCount || 0; // Determine color based on score let scoreColor = '#48bb78'; // Green let scoreStatus = 'Excellent'; if (score < 70) { scoreColor = '#f56565'; // Red scoreStatus = 'Needs Attention'; } else if (score < 85) { scoreColor = '#ed8936'; // Orange scoreStatus = 'Good'; } else if (score < 100) { scoreColor = '#48bb78'; // Green scoreStatus = 'Very Good'; } else if (score === 100) { scoreColor = '#48bb78'; // Green scoreStatus = 'Excellent'; } // Only override to red if score is below 70 OR (invalid AND low errors) if (!isValid && score < 70) { scoreColor = '#f56565'; // Red scoreStatus = 'Needs Attention'; } // Update header badge const validationBadge = document.getElementById('componentValidationBadge'); const validationScoreSpan = document.getElementById('validationScore'); if (validationBadge && validationScoreSpan) { validationBadge.style.display = 'inline-flex'; validationBadge.style.backgroundColor = scoreColor + '20'; validationBadge.style.borderColor = scoreColor + '50'; validationBadge.style.color = scoreColor; validationScoreSpan.textContent = score; validationBadge.title = `Quality Score: ${score}/100 - Click to see details`; validationBadge.style.cursor = 'pointer'; // Add click event to scroll to validation section validationBadge.onclick = function() { const validationSection = document.getElementById('validationSection'); if (validationSection) { validationSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Add a highlight animation validationSection.style.animation = 'highlight 1s ease'; setTimeout(() => { validationSection.style.animation = ''; }, 1000); } }; } // Update score circle const scoreCircle = document.getElementById('scoreCircle'); const scoreValue = document.getElementById('scoreValue'); if (scoreCircle && scoreValue) { scoreCircle.style.borderColor = scoreColor; scoreValue.textContent = score; scoreValue.style.color = scoreColor; } // Update score status const scoreStatusEl = document.getElementById('scoreStatus'); if (scoreStatusEl) { scoreStatusEl.textContent = scoreStatus; scoreStatusEl.style.color = scoreColor; } // Update inline stats const inlineErrors = document.getElementById('inlineErrors'); const inlineWarnings = document.getElementById('inlineWarnings'); if (inlineErrors) { inlineErrors.textContent = errors; inlineErrors.style.color = errors > 0 ? '#f56565' : '#48bb78'; } if (inlineWarnings) { inlineWarnings.textContent = warnings; inlineWarnings.style.color = warnings > 0 ? '#ed8936' : '#48bb78'; } // Render validation checks with detailed messages const validationChecks = document.getElementById('validationChecks'); if (validationChecks && security.validators) { const checkDescriptions = { structural: 'Verifies file format, YAML frontmatter, required fields, and encoding', integrity: 'Checks for tampering using SHA256 hash and version tracking', semantic: 'Detects malicious patterns, prompt injection, and dangerous commands', references: 'Validates external URLs and prevents SSRF attacks', provenance: 'Confirms author metadata and repository information' }; const checkLabels = { structural: 'Structure', integrity: 'Integrity', semantic: 'Content Safety', references: 'references', provenance: 'Source' }; validationChecks.innerHTML = ''; Object.entries(security.validators).forEach(([key, validatorData]) => { // Handle both old format (boolean) and new format (object with detailed data) const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid; const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0; const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0; const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0; const hasDetails = hasErrors || hasWarnings || hasInfo || passed; // Determine status: passed, warning, or failed let status = 'passed'; let icon = '✓'; if (hasErrors) { status = 'failed'; icon = '✗'; } else if (hasWarnings) { status = 'warning'; icon = '⚠'; } const checkItem = document.createElement('div'); checkItem.className = `validation-check-item ${status}`; checkItem.style.cursor = 'pointer'; // Build HTML for check item checkItem.innerHTML = `
${icon} ${checkLabels[key] || key}
${typeof validatorData === 'object' ? ` ${validatorData.errorCount > 0 ? `${validatorData.errorCount} errors` : ''} ${validatorData.warningCount > 0 ? `${validatorData.warningCount} warnings` : ''} ` : ''}
`; // Add click event listener - all items are clickable now checkItem.addEventListener('click', () => { window.openValidationModal(key, checkLabels[key] || key, validatorData, checkDescriptions[key]); }); validationChecks.appendChild(checkItem); }); } } async renderMetadataSection() { const metadataSection = document.getElementById('metadataSection'); // Check if component has metadata fields directly from frontmatter const hasDirectAuthor = this.component.author && this.component.author.trim() !== ''; const hasDirectRepo = this.component.repo && this.component.repo.trim() !== ''; const hasDirectVersion = this.component.version && this.component.version.trim() !== ''; const hasDirectLicense = this.component.license && this.component.license.trim() !== ''; const hasDirectKeywords = this.component.keywords && this.component.keywords.length > 0; // For agents, try to load marketplace data let agentMetadata = null; if (this.component.type === 'agent') { try { const marketplace = await this.loadComponentsMarketplace(); if (marketplace && marketplace.agents) { const componentName = this.component.name.replace('.md', ''); agentMetadata = marketplace.agents.find( agent => agent.name === componentName ); } } catch (error) { console.error('Error loading marketplace metadata:', error); } } // Show metadata section if we have any metadata const hasMetadata = hasDirectAuthor || hasDirectRepo || hasDirectVersion || hasDirectLicense || hasDirectKeywords || agentMetadata; if (!hasMetadata) { if (metadataSection) metadataSection.style.display = 'none'; return; } // Show metadata section if (metadataSection) metadataSection.style.display = 'block'; // Populate version - prioritize direct version field, then marketplace const versionElement = document.getElementById('metadataVersion'); if (versionElement) { const version = this.component.version || agentMetadata?.version || '--'; versionElement.textContent = version; } // Populate author - prioritize direct author field, then marketplace const authorElement = document.getElementById('metadataAuthor'); if (authorElement) { if (hasDirectAuthor) { authorElement.textContent = this.component.author; } else if (agentMetadata) { const authorName = agentMetadata.author?.name || agentMetadata.author || '--'; authorElement.textContent = authorName; } else { authorElement.textContent = '--'; } } // Populate license - prioritize direct license field, then marketplace const licenseElement = document.getElementById('metadataLicense'); if (licenseElement) { const license = this.component.license || agentMetadata?.license || '--'; licenseElement.textContent = license; } // Populate repository - prioritize direct repo field, then marketplace const repositoryLink = document.getElementById('metadataRepository'); const repositoryNone = document.getElementById('metadataRepositoryNone'); let repositoryUrl = null; // Priority 1: Direct repo field if (hasDirectRepo) { repositoryUrl = this.component.repo; } // Priority 2: Marketplace repository else if (agentMetadata?.repository) { repositoryUrl = agentMetadata.repository; } if (repositoryUrl) { if (repositoryLink) { repositoryLink.href = repositoryUrl; repositoryLink.style.display = 'inline-flex'; } if (repositoryNone) { repositoryNone.style.display = 'none'; } } else { if (repositoryLink) { repositoryLink.style.display = 'none'; } if (repositoryNone) { repositoryNone.style.display = 'inline'; } } // Populate keywords - prioritize direct keywords field, then marketplace const keywordsContainer = document.getElementById('metadataKeywords'); if (keywordsContainer) { const keywords = this.component.keywords || agentMetadata?.keywords || []; if (keywords && keywords.length > 0) { keywordsContainer.innerHTML = ''; keywords.forEach(keyword => { const keywordChip = document.createElement('span'); keywordChip.className = 'metadata-keyword'; keywordChip.textContent = keyword; keywordsContainer.appendChild(keywordChip); }); } else { keywordsContainer.innerHTML = '--'; } } } async loadComponentsMarketplace() { try { // Check if components marketplace is already cached if (this.componentsMarketplace) { return this.componentsMarketplace; } // Use the already loaded components data from dataLoader const componentsData = await this.dataLoader.loadAllComponents(); if (componentsData && componentsData.componentsMarketplace) { this.componentsMarketplace = componentsData.componentsMarketplace; return this.componentsMarketplace; } // Fallback: try to fetch marketplace.json directly from components/.claude-plugin/ const marketplaceResponse = await fetch('https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/.claude-plugin/marketplace.json'); if (marketplaceResponse.ok) { this.componentsMarketplace = await marketplaceResponse.json(); return this.componentsMarketplace; } console.warn('No components marketplace found'); return null; } catch (error) { console.error('Error loading components marketplace:', error); return null; } } renderInstallationSection() { const componentPath = this.getCleanPath(); const basicInstallCommand = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`; // Update basic installation command const basicInstallElement = document.getElementById('basicInstallCommand'); if (basicInstallElement) { basicInstallElement.textContent = basicInstallCommand; } // Handle agent-specific sections if (this.component.type === 'agent') { this.renderGlobalAgentSection(componentPath); this.renderCloudSandboxSection(componentPath); } else { // Hide agent-specific sections for non-agents const globalAgentSection = document.getElementById('globalAgentSection'); const cloudSandboxSection = document.getElementById('cloudSandboxSection'); if (globalAgentSection) globalAgentSection.style.display = 'none'; if (cloudSandboxSection) cloudSandboxSection.style.display = 'none'; } } renderGlobalAgentSection(componentPath) { const globalAgentSection = document.getElementById('globalAgentSection'); const globalAgentCommand = `npx claude-code-templates@latest --create-agent ${componentPath}`; const globalUsageCommand = `${componentPath.split('/').pop()} "your prompt here"`; const globalAgentCommandElement = document.getElementById('globalAgentCommand'); const globalUsageCommandElement = document.getElementById('globalUsageCommand'); if (globalAgentCommandElement) { globalAgentCommandElement.textContent = globalAgentCommand; } if (globalUsageCommandElement) { globalUsageCommandElement.textContent = globalUsageCommand; } if (globalAgentSection) { globalAgentSection.style.display = 'block'; } } renderCloudSandboxSection(componentPath) { const cloudSandboxSection = document.getElementById('cloudSandboxSection'); // Update all sandbox command examples with the component path const e2bSandboxCommand = `npx claude-code-templates@latest --sandbox e2b --agent=${componentPath} --prompt "your development task"`; const cloudflareSandboxCommand = `npx claude-code-templates@latest --sandbox cloudflare --agent=${componentPath} --prompt "your development task"`; const dockerSandboxCommand = `npx claude-code-templates@latest --sandbox docker --agent=${componentPath} --prompt "your development task"`; const e2bSandboxCommandElement = document.getElementById('e2bSandboxCommand'); const cloudflareSandboxCommandElement = document.getElementById('cloudflareSandboxCommand'); const dockerSandboxCommandElement = document.getElementById('dockerSandboxCommand'); if (e2bSandboxCommandElement) { e2bSandboxCommandElement.textContent = e2bSandboxCommand; } if (cloudflareSandboxCommandElement) { cloudflareSandboxCommandElement.textContent = cloudflareSandboxCommand; } if (dockerSandboxCommandElement) { dockerSandboxCommandElement.textContent = dockerSandboxCommand; } if (cloudSandboxSection) { cloudSandboxSection.style.display = 'block'; } } async renderComponentCode() { try { let content = this.component.content || 'No content available.'; let language = 'plaintext'; // Determine language based on file extension if (this.component.path) { const extension = this.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) { console.warn('Could not parse JSON content:', e); } break; case 'js': language = 'javascript'; break; case 'yml': case 'yaml': language = 'yaml'; break; } } // Collect line numbers with errors and warnings const errorLines = new Set(); const warningLines = new Set(); if (this.component.security && this.component.security.validators) { Object.values(this.component.security.validators).forEach(validator => { if (validator.errors) { validator.errors.forEach(error => { // Check direct line property if (error.line) { errorLines.add(error.line); } // Check examples array if (error.examples && error.examples.length > 0) { error.examples.forEach(example => { if (example.line) { errorLines.add(example.line); } }); } }); } if (validator.warnings) { validator.warnings.forEach(warning => { // Check direct line property if (warning.line) { warningLines.add(warning.line); } // Check examples array if (warning.examples && warning.examples.length > 0) { warning.examples.forEach(example => { if (example.line) { warningLines.add(example.line); } }); } }); } }); } // Update code language indicator const codeLanguageElement = document.getElementById('codeLanguage'); if (codeLanguageElement) { codeLanguageElement.textContent = language; } // Render code with syntax highlighting const codeElement = document.querySelector('#codeContent code'); if (codeElement) { codeElement.innerHTML = this.highlightCode(content, language); codeElement.className = `language-${language}`; } // Generate line numbers with error/warning indicators const lines = content.split('\n'); const lineNumbers = lines.map((_, index) => { const lineNum = index + 1; let className = ''; let style = ''; let title = ''; if (errorLines.has(lineNum)) { className = 'line-with-error'; style = 'background-color: rgba(245, 101, 101, 0.15); color: #f56565; font-weight: 600; border-left: 3px solid #f56565; padding-left: 5px; cursor: pointer;'; title = 'Line has validation errors - Click to see details'; } else if (warningLines.has(lineNum)) { className = 'line-with-warning'; style = 'background-color: rgba(237, 137, 54, 0.15); color: #ed8936; font-weight: 600; border-left: 3px solid #ed8936; padding-left: 5px; cursor: pointer;'; title = 'Line has validation warnings - Click to see details'; } return `${lineNum}`; }).join(''); const lineNumbersElement = document.getElementById('lineNumbers'); if (lineNumbersElement) { lineNumbersElement.innerHTML = lineNumbers; // Add click event listeners to error/warning line numbers const clickableLines = lineNumbersElement.querySelectorAll('.line-with-error, .line-with-warning'); clickableLines.forEach(lineSpan => { lineSpan.addEventListener('click', () => { // Find which validator has the error for this line const lineNum = parseInt(lineSpan.getAttribute('data-line-number')); if (this.component.security && this.component.security.validators) { // Look for the validator with this line number for (const [validatorKey, validatorData] of Object.entries(this.component.security.validators)) { if (typeof validatorData === 'object') { // Check if this validator has errors/warnings for this line const hasError = validatorData.errors && validatorData.errors.some(error => { if (error.line === lineNum) return true; if (error.examples && error.examples.some(ex => ex.line === lineNum)) return true; return false; }); const hasWarning = validatorData.warnings && validatorData.warnings.some(warning => { if (warning.line === lineNum) return true; if (warning.examples && warning.examples.some(ex => ex.line === lineNum)) return true; return false; }); if (hasError || hasWarning) { // Found the validator with this line, open its modal const checkLabels = { structural: 'Structure', integrity: 'Integrity', semantic: 'Content Safety', references: 'references', provenance: 'Source' }; const checkDescriptions = { structural: 'Verifies file format, YAML frontmatter, required fields, and encoding', integrity: 'Checks for tampering using SHA256 hash and version tracking', semantic: 'Detects malicious patterns, prompt injection, and dangerous commands', references: 'Validates external URLs and prevents SSRF attacks', provenance: 'Confirms author metadata and repository information' }; window.openValidationModal( validatorKey, checkLabels[validatorKey] || validatorKey, validatorData, checkDescriptions[validatorKey] ); break; } } } } }); }); } // Add corresponding highlighting to code lines if (errorLines.size > 0 || warningLines.size > 0) { const codeLines = codeElement.innerHTML.split('\n'); const highlightedLines = codeLines.map((line, index) => { const lineNum = index + 1; if (errorLines.has(lineNum)) { return `${line}`; } else if (warningLines.has(lineNum)) { return `${line}`; } return line; }); codeElement.innerHTML = highlightedLines.join('\n'); } // Sync scroll between line numbers and code content const codeContentDiv = document.getElementById('codeContent'); if (lineNumbersElement && codeContentDiv) { codeContentDiv.addEventListener('scroll', function() { lineNumbersElement.scrollTop = codeContentDiv.scrollTop; }); } } catch (error) { console.error('Error rendering component code:', error); const codeElement = document.querySelector('#codeContent code'); if (codeElement) { codeElement.textContent = 'Error loading component content.'; } } } loadGiscusComments() { const container = document.getElementById('giscusContainer'); if (!container) return; container.innerHTML = ''; const script = document.createElement('script'); script.src = 'https://giscus.app/client.js'; script.setAttribute('data-repo', 'davila7/claude-code-templates'); script.setAttribute('data-repo-id', 'R_kgDOPGh7XA'); script.setAttribute('data-category', 'Comments'); script.setAttribute('data-category-id', 'DIC_kwDOPGh7XM4C2H0y'); script.setAttribute('data-mapping', 'pathname'); script.setAttribute('data-strict', '0'); script.setAttribute('data-reactions-enabled', '0'); script.setAttribute('data-emit-metadata', '0'); script.setAttribute('data-input-position', 'bottom'); script.setAttribute('data-theme', 'dark'); script.setAttribute('data-lang', 'en'); script.setAttribute('crossorigin', 'anonymous'); script.async = true; container.appendChild(script); } renderSidebarQuickInstall() { const commandEl = document.getElementById('quickInstallCommand'); if (commandEl) { const componentPath = this.getCleanPath(); commandEl.textContent = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`; } // Populate sidebar GitHub link const sidebarLink = document.getElementById('githubLinkSidebar'); if (sidebarLink) { sidebarLink.href = this.generateGitHubURL(); } } activateTabFromHash() { const hash = window.location.hash.slice(1); if (hash) { this.switchTab(hash); } } switchTab(tabName) { const validTabs = ['overview', 'code', 'installation']; if (!validTabs.includes(tabName)) return; // Update buttons document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tabName); }); // Update panes document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.toggle('active', pane.id === `tab-${tabName}`); }); } renderGitHubLink() { const githubUrl = this.generateGitHubURL(); const githubLinkElement = document.getElementById('githubLink'); if (githubLinkElement) { githubLinkElement.href = githubUrl; } } generateGitHubURL() { let githubUrl = 'https://github.com/davila7/claude-code-templates/'; if (this.component.type === 'template') { githubUrl += `tree/main/cli-tool/templates/${this.component.folderPath || ''}`; } else { const componentPath = this.component.path || this.component.name; githubUrl += `blob/main/cli-tool/components/${this.component.type}s/${componentPath}`; } return githubUrl; } setupEventListeners() { // Track copy command clicks on installation buttons document.addEventListener('click', (e) => { const copyBtn = e.target.closest('.copy-btn, .quick-copy-btn'); if (copyBtn && this.component) { window.eventTracker?.track('copy_command', { component_type: this.component.type, component_name: this.component.name, source: 'detail_page' }); } }); // Tab switching document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; this.switchTab(tab); history.replaceState(null, '', `#${tab}`); }); }); // Handle hash changes window.addEventListener('hashchange', () => { this.activateTabFromHash(); }); // Handle browser back/forward buttons window.addEventListener('popstate', () => { this.loadComponentFromURL(); }); // Handle Add to Stack button const addToCartBtn = document.getElementById('addToCartBtn'); if (addToCartBtn) { addToCartBtn.addEventListener('click', (e) => { e.preventDefault(); if (this.component) { if (typeof addToCart === 'function') { // Convert component type to plural format const componentType = this.getComponentTypePlural(); if (!componentType) { console.error('Failed to get component type plural'); alert('Unable to determine component type. Please refresh the page.'); return; } if (this.component.name && this.component.path) { // Create component object that matches cart manager expectations // Clean component name (remove .md extension and format properly) const cleanName = this.getCleanComponentName(); const componentItem = { name: cleanName, path: this.component.path, category: this.component.category }; addToCart(componentItem, componentType); } else { console.error('Missing component name or path:', this.component); alert('Unable to add component: missing component information'); } } else { console.error('Cart functionality not available'); // Fallback: show a message or redirect to main page alert('Cart functionality not available. Please return to the main page to add components to your stack.'); } } else { console.error('No component data available'); alert('Component data not loaded. Please refresh the page.'); } }); } } getComponentType() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('type'); } getComponentTypePlural() { // Get type from the component object itself const type = this.component?.type; if (!type) { console.error('Component type not found in component object'); return null; } // Convert singular type to plural for cart manager const typeMapping = { 'agent': 'agents', 'command': 'commands', 'setting': 'settings', 'hook': 'hooks', 'mcp': 'mcps', 'template': 'templates' }; return typeMapping[type] || type + 's'; } getComponentFromURL() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('name') || urlParams.get('path'); } getCleanComponentName() { if (!this.component || !this.component.name) { return 'Unknown Component'; } let cleanName = this.component.name; // Remove .md extension if present if (cleanName.endsWith('.md')) { cleanName = cleanName.slice(0, -3); } // 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; } updatePageMetadata() { const cleanName = this.getCleanComponentName(this.component.name); const description = this.getComponentDescription(); const typeCapitalized = this.component.type.charAt(0).toUpperCase() + this.component.type.slice(1); const category = this.component.category || 'Development'; // Enhanced page title with component type and category const pageTitle = `${cleanName} ${typeCapitalized} - Claude Code Templates`; const enhancedDescription = `${description} | ${typeCapitalized} for ${category} | Claude Code Templates - AI-powered development tools`; // Generate proper canonical URL for SEO const canonicalURL = this.generateCanonicalURL(); // Update page title document.title = pageTitle; const pageTitleElement = document.getElementById('page-title'); if (pageTitleElement) { pageTitleElement.textContent = pageTitle; } // Update meta description const metaDescription = document.querySelector('meta[name="description"]'); if (metaDescription) { metaDescription.content = enhancedDescription; } // Update keywords const metaKeywords = document.querySelector('meta[name="keywords"]'); if (metaKeywords) { metaKeywords.content = `${cleanName}, ${this.component.type}, ${category}, Claude Code, AI development, automation, ${this.component.type}s, templates`; } // Update canonical URL const canonicalLink = document.getElementById('canonical-url'); if (canonicalLink) { canonicalLink.href = canonicalURL; } // Update Open Graph meta tags const ogUrl = document.getElementById('og-url'); const ogTitle = document.getElementById('og-title'); const ogDescription = document.getElementById('og-description'); if (ogUrl) ogUrl.content = canonicalURL; if (ogTitle) ogTitle.content = pageTitle; if (ogDescription) ogDescription.content = enhancedDescription; // Update Twitter meta tags const twitterUrl = document.getElementById('twitter-url'); const twitterTitle = document.getElementById('twitter-title'); const twitterDescription = document.getElementById('twitter-description'); if (twitterUrl) twitterUrl.content = canonicalURL; if (twitterTitle) twitterTitle.content = pageTitle; if (twitterDescription) twitterDescription.content = enhancedDescription; // Add structured data for better SEO this.addStructuredData(cleanName, description, typeCapitalized, category, canonicalURL); } generateCanonicalURL() { // Generate SEO-friendly canonical URL const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.includes('5500'); if (!isLocal && this.component.type && this.component.name) { // Use SEO-friendly URL for production let cleanName = this.component.name; if (cleanName.endsWith('.md')) { cleanName = cleanName.slice(0, -3); } if (cleanName.endsWith('.json')) { cleanName = cleanName.slice(0, -5); } const baseUrl = window.location.origin; return `${baseUrl}/component/${encodeURIComponent(this.component.type)}/${encodeURIComponent(cleanName)}`; } // Fallback to current URL return window.location.href; } addStructuredData(name, description, type, category, url) { // Remove existing structured data const existingScript = document.getElementById('component-structured-data'); if (existingScript) { existingScript.remove(); } // Add new structured data const structuredData = { "@context": "https://schema.org", "@type": "SoftwareApplication", "name": name, "applicationCategory": "DeveloperApplication", "applicationSubCategory": `${type} Component`, "description": description, "operatingSystem": ["Windows", "macOS", "Linux"], "url": url, "author": { "@type": "Organization", "name": "Claude Code Templates Community" }, "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, "keywords": `${name}, ${type}, ${category}, Claude Code, AI development`, "programmingLanguage": type === "command" ? "Shell" : "Configuration", "relatedLink": "https://www.anthropic.com/claude-code" }; const script = document.createElement('script'); script.id = 'component-structured-data'; script.type = 'application/ld+json'; script.textContent = JSON.stringify(structuredData, null, 2); document.head.appendChild(script); } // Utility methods 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).replace(/\.0$/, '') + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; } return num.toString(); } getComponentDescription() { let description = this.component.description || ''; if (!description && this.component.content) { const descMatch = this.component.content.match(/description:\s*(.+?)(?:\n|$)/); if (descMatch) { description = descMatch[1].trim().replace(/^["']|["']$/g, ''); } else { const lines = this.component.content.split('\n'); const firstParagraph = lines.find(line => line.trim() && !line.startsWith('---') && !line.startsWith('#') ); if (firstParagraph) { description = firstParagraph.trim(); } } } if (!description) { description = `A ${this.component.type} component for enhanced development workflow.`; } return description; } getCleanPath() { let componentPath = this.component.path || this.component.name; // Remove file extensions if (componentPath.endsWith('.md') || componentPath.endsWith('.json')) { componentPath = componentPath.replace(/\.(md|json)$/, ''); } return componentPath; } highlightCode(content, language) { // Basic syntax highlighting (same as modal) let highlighted = content .replace(/&/g, '&') .replace(//g, '>'); if (language === 'markdown' || language === 'yaml') { // Highlight YAML/Markdown frontmatter keys (blue) highlighted = highlighted.replace(/^([a-zA-Z_-]+):/gm, '$1:'); // Highlight strings in quotes (orange) highlighted = highlighted.replace(/"([^&]+?)"/g, '"$1"'); // Highlight important keywords (light blue) 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, '$1'); // Highlight markdown headers (blue) highlighted = highlighted.replace(/^(#+)\s*(.+)$/gm, '$1 $2'); // Highlight code in backticks highlighted = highlighted.replace(/`([^`]+)`/g, '$1'); // Highlight YAML separators highlighted = highlighted.replace(/^---$/gm, '---'); } return highlighted; } showError(message) { console.error('Component page error:', message); document.getElementById('loadingState').style.display = 'none'; document.getElementById('componentContent').style.display = 'none'; document.getElementById('errorState').style.display = 'block'; // Update error message if needed const errorElement = document.querySelector('#errorState p'); if (errorElement && message !== 'Missing component parameters in URL') { errorElement.textContent = message; } } // Static method to create URL for component static 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()}`; } } // Global function for creating component URLs (used by other scripts) function createComponentURL(type, name, path) { return ComponentPageManager.createComponentURL(type, name, path); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { window.componentPageManager = new ComponentPageManager(); }); // Export for other scripts window.ComponentPageManager = ComponentPageManager; // Component Share functionality function toggleComponentShareDropdown() { const shareDropdown = document.getElementById('componentShareDropdown'); if (shareDropdown) { shareDropdown.classList.toggle('open'); } } function shareComponentOnTwitter() { const componentManager = window.componentPageManager; let message; if (componentManager && componentManager.component) { const cleanName = componentManager.getCleanComponentName(componentManager.component.name); const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1); const category = componentManager.component.category || 'Development'; const canonicalURL = componentManager.generateCanonicalURL(); message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code! Perfect for ${category.toLowerCase()} workflows with AI-powered automation. ${canonicalURL} #ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`; } else { // Fallback message const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component'; const currentURL = window.location.href; message = `🚀 Check out this ${componentTitle} for Claude Code! Perfect for AI-powered development workflows. ${currentURL} #ClaudeCode #AI #Development`; } const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}`; window.open(twitterUrl, '_blank'); // Close dropdown after sharing const shareDropdown = document.getElementById('componentShareDropdown'); if (shareDropdown) { shareDropdown.classList.remove('open'); } } function shareComponentOnThreads() { const componentManager = window.componentPageManager; let message; if (componentManager && componentManager.component) { const cleanName = componentManager.getCleanComponentName(componentManager.component.name); const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1); const category = componentManager.component.category || 'Development'; const canonicalURL = componentManager.generateCanonicalURL(); message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code! Perfect for ${category.toLowerCase()} workflows with AI-powered automation. ${canonicalURL} #ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`; } else { // Fallback message const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component'; const currentURL = window.location.href; message = `🚀 Check out this ${componentTitle} for Claude Code! Perfect for AI-powered development workflows. ${currentURL} #ClaudeCode #AI #Development`; } const threadsUrl = `https://threads.net/intent/post?text=${encodeURIComponent(message)}`; window.open(threadsUrl, '_blank'); // Close dropdown after sharing const shareDropdown = document.getElementById('componentShareDropdown'); if (shareDropdown) { shareDropdown.classList.remove('open'); } } // Close dropdown when clicking outside document.addEventListener('click', (e) => { const shareDropdown = document.getElementById('componentShareDropdown'); if (shareDropdown && !shareDropdown.contains(e.target)) { shareDropdown.classList.remove('open'); } }); // Validation Modal System window.openValidationModal = function(validatorKey, validatorLabel, validatorData, description) { // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.className = 'modal-overlay'; modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 20px; backdrop-filter: blur(3px); overflow: hidden; `; // Create modal container const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background: #1a1a1a; border: 1px solid #333; border-radius: 8px; max-width: 700px; max-height: 80vh; width: 100%; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); overflow: hidden; `; // Handle both old format (boolean) and new format (object) const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid; const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0; const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0; const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0; // Determine status let status = 'passed'; let statusColor = '#48bb78'; let statusText = 'All Checks Passed'; let statusIcon = '✓'; if (hasErrors) { status = 'failed'; statusColor = '#f56565'; statusText = 'Validation Failed'; statusIcon = '✗'; } else if (hasWarnings) { status = 'warning'; statusColor = '#ed8936'; statusText = 'Validation Passed with Warnings'; statusIcon = '⚠'; } // Build modal content - Header (fixed) let modalHeader = `

${validatorLabel}

${description || 'Validation check details'}

${statusIcon} ${statusText}
`; // Build modal body content (scrollable) let modalBodyContent = ''; // Add errors section if present if (hasErrors) { modalBodyContent += `

Errors (${validatorData.errorCount || validatorData.errors.length})

`; validatorData.errors.forEach((error, index) => { // Build location info if available let locationInfo = ''; if (error.position || error.line) { locationInfo = `
${error.position ? `Line ${error.position}` : `Line ${error.line}` + (error.column ? `:${error.column}` : '')}
`; } // Build line text preview if available let lineTextPreview = ''; if (error.lineText) { lineTextPreview = `
${error.lineText.replace(//g, '>')}
`; } // Handle examples array (for patterns with multiple matches) let examplesSection = ''; if (error.examples && error.examples.length > 0) { examplesSection = `
Found ${error.examples.length} occurrence${error.examples.length > 1 ? 's' : ''}:
`; error.examples.slice(0, 5).forEach((example, exIdx) => { examplesSection += `
${example.position || `Line ${example.line}`} ${example.text ? ` "${example.text.replace(//g, '>')}" ` : ''}
${example.lineText ? `
${example.lineText.replace(//g, '>')}
` : ''}
`; }); if (error.examples.length > 5) { examplesSection += `
... and ${error.examples.length - 5} more occurrence${error.examples.length - 5 > 1 ? 's' : ''}
`; } examplesSection += `
`; } modalBodyContent += `
${error.code || 'ERROR'}
${locationInfo}
${error.message || error}
${lineTextPreview} ${examplesSection} ${error.context && !error.examples ? `
${JSON.stringify(error.context, null, 2).replace(/\n/g, '
').replace(/ /g, ' ')}
` : ''}
`; }); modalBodyContent += `
`; } // Add warnings section if present if (hasWarnings) { modalBodyContent += `

Warnings (${validatorData.warningCount || validatorData.warnings.length})

`; validatorData.warnings.forEach((warning, index) => { // Build location info if available let locationInfo = ''; if (warning.position || warning.line) { locationInfo = `
${warning.position ? `Line ${warning.position}` : `Line ${warning.line}` + (warning.column ? `:${warning.column}` : '')}
`; } // Build line text preview if available let lineTextPreview = ''; if (warning.lineText) { lineTextPreview = `
${warning.lineText.replace(//g, '>')}
`; } // Handle examples array (for patterns with multiple matches) let examplesSection = ''; if (warning.examples && warning.examples.length > 0) { examplesSection = `
Found ${warning.examples.length} occurrence${warning.examples.length > 1 ? 's' : ''}:
`; warning.examples.slice(0, 5).forEach((example, exIdx) => { examplesSection += `
${example.position || `Line ${example.line}`} ${example.text ? ` "${example.text.replace(//g, '>')}" ` : ''}
${example.lineText ? `
${example.lineText.replace(//g, '>')}
` : ''}
`; }); if (warning.examples.length > 5) { examplesSection += `
... and ${warning.examples.length - 5} more occurrence${warning.examples.length - 5 > 1 ? 's' : ''}
`; } examplesSection += `
`; } modalBodyContent += `
${warning.code || 'WARNING'}
${locationInfo}
${warning.message || warning}
${lineTextPreview} ${examplesSection} ${warning.context && !warning.examples ? `
${JSON.stringify(warning.context, null, 2).replace(/\n/g, '
').replace(/ /g, ' ')}
` : ''}
`; }); modalBodyContent += `
`; } // Add success/info section if everything passed if (!hasErrors && !hasWarnings && (hasInfo || passed)) { // Generate validator-specific success summary let successSummary = ''; switch(validatorKey) { case 'structural': successSummary = `
This component has a valid structure with proper formatting:
`; break; case 'integrity': successSummary = `
This component's integrity has been verified:
`; break; case 'semantic': successSummary = `
This component is safe and contains no malicious content:
`; break; case 'references': successSummary = `
All external references have been validated:
`; break; case 'provenance': successSummary = `
Component origin and authorship have been confirmed:
`; break; default: successSummary = `
This component has been validated and meets all ${validatorLabel.toLowerCase()} requirements.
`; } modalBodyContent += `

Validation Passed

All ${validatorLabel} checks passed successfully
${successSummary} `; // Add info items if present if (hasInfo) { modalBodyContent += `
Additional Details:
`; validatorData.info.forEach((info, index) => { modalBodyContent += `
• ${info.message || info}
`; }); modalBodyContent += `
`; } modalBodyContent += `
`; } // Build modal footer (fixed) let modalFooter = `
`; // Assemble the complete modal with scrollable body modalContainer.innerHTML = ` ${modalHeader}
${modalBodyContent}
${modalFooter}
`; modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); // Close modal handlers function closeModal() { document.body.removeChild(modalOverlay); } document.getElementById('closeValidationModal').addEventListener('click', closeModal); document.getElementById('closeValidationModalBtn').addEventListener('click', closeModal); // Close on overlay click (but not on modal content click) modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { closeModal(); } }); // Close on Escape key function handleEscape(e) { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleEscape); } } document.addEventListener('keydown', handleEscape); };