2195 lines
82 KiB
JavaScript
2195 lines
82 KiB
JavaScript
// GitHub repository configuration
|
||
const GITHUB_CONFIG = {
|
||
owner: 'davila7',
|
||
repo: 'claude-code-templates',
|
||
branch: 'main',
|
||
templatesPath: 'cli-tool/src/templates.js'
|
||
};
|
||
|
||
// Framework logos using Devicon CDN (https://devicon.dev/)
|
||
const FRAMEWORK_ICONS = {
|
||
// Languages
|
||
'common': 'devicon-gear-plain', // Generic gear icon
|
||
'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', // Use Ruby icon for Sinatra
|
||
|
||
// Default fallback
|
||
'default': 'devicon-devicon-plain'
|
||
};
|
||
|
||
let templatesData = null;
|
||
|
||
// Fetch templates configuration from GitHub
|
||
async function fetchTemplatesConfig() {
|
||
const grid = document.getElementById('unifiedGrid') || document.getElementById('templatesGrid');
|
||
grid.innerHTML = '<div class="loading">Loading templates from GitHub...</div>';
|
||
|
||
try {
|
||
// Add cache-busting parameter to ensure we get the latest version
|
||
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();
|
||
|
||
// Parse the JavaScript file to extract TEMPLATES_CONFIG
|
||
templatesData = parseTemplatesConfig(templateFileContent);
|
||
|
||
if (templatesData) {
|
||
generateTemplateCards();
|
||
} else {
|
||
throw new Error('Failed to parse templates configuration');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error fetching templates:', error);
|
||
grid.innerHTML = `
|
||
<div class="error-message">
|
||
<h3>Error loading templates</h3>
|
||
<p>Could not fetch templates from GitHub. Please try again later.</p>
|
||
<button onclick="displayTemplates()" class="retry-btn">Retry</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Parse the templates.js file content to extract TEMPLATES_CONFIG
|
||
function parseTemplatesConfig(fileContent) {
|
||
try {
|
||
// Extract TEMPLATES_CONFIG object from the file
|
||
const configMatch = fileContent.match(/const TEMPLATES_CONFIG = ({[\s\S]*?});/);
|
||
if (!configMatch) {
|
||
throw new Error('TEMPLATES_CONFIG not found in file');
|
||
}
|
||
|
||
// Clean up the extracted object string and make it valid JSON
|
||
let configString = configMatch[1];
|
||
|
||
// Replace single quotes with double quotes
|
||
configString = configString.replace(/'/g, '"');
|
||
|
||
// Handle object property names without quotes
|
||
configString = configString.replace(/(\w+):/g, '"$1":');
|
||
|
||
// Remove trailing commas
|
||
configString = configString.replace(/,(\s*[}\]])/g, '$1');
|
||
|
||
// Parse the JSON
|
||
const config = JSON.parse(configString);
|
||
|
||
return config;
|
||
} catch (error) {
|
||
console.error('Error parsing templates config:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Generate template cards from fetched data
|
||
function generateTemplateCards() {
|
||
const grid = document.getElementById('unifiedGrid');
|
||
grid.innerHTML = '';
|
||
|
||
if (!templatesData) {
|
||
grid.innerHTML = '<div class="error-message">No templates data available</div>';
|
||
return;
|
||
}
|
||
|
||
// Add the "Add New Template" card first
|
||
const addTemplateCard = createAddTemplateCard();
|
||
grid.appendChild(addTemplateCard);
|
||
|
||
Object.entries(templatesData).forEach(([languageKey, languageData]) => {
|
||
// Skip the 'common' template as we're replacing it with the Add Template card
|
||
if (languageKey === 'common') {
|
||
return;
|
||
}
|
||
|
||
// Create base language card (no framework)
|
||
const baseCard = createTemplateCard(languageKey, languageData, 'none', {
|
||
name: languageData.name,
|
||
icon: getFrameworkIcon(languageKey),
|
||
command: `npx claude-code-templates@latest --template=${languageKey} --yes`
|
||
});
|
||
grid.appendChild(baseCard);
|
||
|
||
// Create framework-specific cards
|
||
if (languageData.frameworks) {
|
||
Object.entries(languageData.frameworks).forEach(([frameworkKey, frameworkData]) => {
|
||
const frameworkCard = createTemplateCard(languageKey, languageData, frameworkKey, {
|
||
name: frameworkData.name,
|
||
icon: getFrameworkIcon(frameworkKey),
|
||
command: `npx claude-code-templates@latest --template=${languageKey} --yes`
|
||
});
|
||
grid.appendChild(frameworkCard);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function createAddTemplateCard() {
|
||
const card = document.createElement('div');
|
||
card.className = 'template-card add-template-card';
|
||
|
||
card.innerHTML = `
|
||
<div class="card-inner">
|
||
<div class="card-front">
|
||
<div class="framework-logo">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
||
</svg>
|
||
</div>
|
||
<h3 class="template-title">Add New Template</h3>
|
||
<p class="template-description">Contribute a new language or framework to the community</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add click handler to open contribution modal directly (no flip)
|
||
card.addEventListener('click', () => {
|
||
showContributeModal();
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
function 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}`;
|
||
|
||
// Get download count for this template
|
||
const templateKey = frameworkKey === 'none' ? languageKey : `${languageKey}/${frameworkKey}`;
|
||
const downloadCount = getDownloadCount(templateKey, 'template');
|
||
const downloadBadge = createDownloadBadge(downloadCount);
|
||
|
||
card.innerHTML = `
|
||
<div class="card-inner">
|
||
<div class="card-front">
|
||
${languageData.comingSoon ? '<div class="coming-soon-badge">Coming Soon</div>' : ''}
|
||
${downloadBadge}
|
||
<div class="framework-logo">
|
||
<i class="${frameworkData.icon} colored"></i>
|
||
</div>
|
||
<h3 class="template-title">${displayName}</h3>
|
||
<p class="template-description">${languageData.description || ''}</p>
|
||
</div>
|
||
<div class="card-back">
|
||
<div class="command-display">
|
||
<h3>Installation Options</h3>
|
||
<div class="command-code">${frameworkData.command}</div>
|
||
<div class="action-buttons">
|
||
<button class="view-files-btn" onclick="showInstallationFiles('${languageKey}', '${frameworkKey}', '${displayName}')">
|
||
📁 View Files
|
||
</button>
|
||
<button class="copy-command-btn" onclick="copyToClipboard('${frameworkData.command}')">
|
||
📋 Copy Command
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Get framework icon from mapping
|
||
function getFrameworkIcon(framework) {
|
||
return FRAMEWORK_ICONS[framework] || FRAMEWORK_ICONS['default'];
|
||
}
|
||
|
||
// Get installation files for a specific template
|
||
function getInstallationFiles(languageKey, frameworkKey) {
|
||
if (!templatesData || !templatesData[languageKey]) {
|
||
return [];
|
||
}
|
||
|
||
const languageData = templatesData[languageKey];
|
||
let files = [...(languageData.files || [])];
|
||
|
||
// Add framework-specific files if applicable
|
||
if (frameworkKey !== 'none' && languageData.frameworks && languageData.frameworks[frameworkKey]) {
|
||
const frameworkData = languageData.frameworks[frameworkKey];
|
||
if (frameworkData.additionalFiles) {
|
||
files = files.concat(frameworkData.additionalFiles);
|
||
}
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
// Show installation files popup
|
||
function showInstallationFiles(languageKey, frameworkKey, displayName) {
|
||
const files = getInstallationFiles(languageKey, frameworkKey);
|
||
|
||
if (files.length === 0) {
|
||
showCopyFeedback('No files to display');
|
||
return;
|
||
}
|
||
|
||
// Generate GitHub folder URL
|
||
const githubFolderUrl = getGithubFolderUrl(languageKey, frameworkKey);
|
||
|
||
// Create modal HTML
|
||
const modalHTML = `
|
||
<div class="modal-overlay" onclick="closeModal()">
|
||
<div class="modal-content" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<h3>📁 Installation Files - ${displayName}</h3>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="modal-description">The following files will be installed in your project:</p>
|
||
<div class="files-table">
|
||
<div class="table-header">
|
||
<div class="column-header">File</div>
|
||
<div class="column-header">Destination</div>
|
||
<div class="column-header">Type</div>
|
||
</div>
|
||
${files.map(file => `
|
||
<div class="table-row">
|
||
<div class="file-source">
|
||
<a href="${getGithubFileUrl(languageKey, frameworkKey, file.source)}" target="_blank" class="file-link">
|
||
${file.source}
|
||
</a>
|
||
</div>
|
||
<div class="file-destination">${file.destination}</div>
|
||
<div class="file-type">${getFileType(file.destination)}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div class="modal-footer">
|
||
<p class="file-count">Total: ${files.length} file${files.length > 1 ? 's' : ''}</p>
|
||
<div class="modal-actions">
|
||
<a href="${githubFolderUrl}" 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 all files on GitHub
|
||
</a>
|
||
</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);
|
||
|
||
// Add event listener for ESC key
|
||
const handleEscape = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeComponentModal();
|
||
document.removeEventListener('keydown', handleEscape);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleEscape);
|
||
}
|
||
|
||
// ===== ANALYTICS AND STATISTICS FUNCTIONALITY =====
|
||
|
||
/**
|
||
* Load and display download statistics from GitHub-generated analytics
|
||
*/
|
||
async function loadDownloadStatistics() {
|
||
try {
|
||
// Fetch analytics data generated by GitHub Actions
|
||
const response = await fetch('analytics/download-stats.json?t=' + Date.now());
|
||
|
||
if (!response.ok) {
|
||
hideStatisticsSection();
|
||
return;
|
||
}
|
||
|
||
const analyticsData = await response.json();
|
||
displayDownloadStatistics(analyticsData);
|
||
|
||
} catch (error) {
|
||
hideStatisticsSection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display download statistics in the UI
|
||
*/
|
||
function displayDownloadStatistics(data) {
|
||
// Update total downloads
|
||
const totalElement = document.getElementById('totalDownloads');
|
||
if (totalElement) {
|
||
totalElement.textContent = formatNumber(data.total_downloads || 0);
|
||
}
|
||
|
||
// Update individual component type counts
|
||
const typeElements = {
|
||
agentDownloads: data.downloads_by_type?.agent || 0,
|
||
commandDownloads: data.downloads_by_type?.command || 0,
|
||
mcpDownloads: data.downloads_by_type?.mcp || 0,
|
||
templateDownloads: data.downloads_by_type?.template || 0
|
||
};
|
||
|
||
Object.entries(typeElements).forEach(([elementId, count]) => {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.textContent = formatNumber(count);
|
||
}
|
||
});
|
||
|
||
// Find and display most popular component
|
||
const popularElement = document.getElementById('popularComponent');
|
||
if (popularElement && data.downloads_by_component) {
|
||
const topComponent = Object.entries(data.downloads_by_component)[0];
|
||
if (topComponent) {
|
||
const [componentName, downloadCount] = topComponent;
|
||
popularElement.textContent = formatComponentNameForDisplay(componentName);
|
||
popularElement.setAttribute('title', `${formatNumber(downloadCount)} downloads`);
|
||
} else {
|
||
popularElement.textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Update last updated timestamp
|
||
const lastUpdatedElement = document.getElementById('statsLastUpdated');
|
||
if (lastUpdatedElement && data.last_updated) {
|
||
const lastUpdated = new Date(data.last_updated);
|
||
lastUpdatedElement.textContent = formatRelativeTime(lastUpdated);
|
||
}
|
||
|
||
// Show the statistics section
|
||
showStatisticsSection();
|
||
|
||
}
|
||
|
||
/**
|
||
* Hide statistics section when data is not available
|
||
*/
|
||
function hideStatisticsSection() {
|
||
// Keep statistics section visible even when data is not available
|
||
// const statsSection = document.getElementById('downloadStatsSection');
|
||
// if (statsSection) {
|
||
// statsSection.style.display = 'none';
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Show statistics section when data is available
|
||
* Note: Stats section has been moved to dedicated page at /download-stats.html
|
||
*/
|
||
function showStatisticsSection() {
|
||
// Stats section removed from main page - no action needed
|
||
}
|
||
|
||
/**
|
||
* Format numbers with thousands separators
|
||
*/
|
||
function formatNumber(num) {
|
||
if (num === 0) return '0';
|
||
if (num < 1000) return num.toString();
|
||
if (num < 1000000) return (num / 1000).toFixed(1) + 'K';
|
||
return (num / 1000000).toFixed(1) + 'M';
|
||
}
|
||
|
||
/**
|
||
* Format component name for display (remove prefixes, capitalize)
|
||
*/
|
||
function formatComponentNameForDisplay(componentName) {
|
||
if (!componentName || componentName === 'unknown') return '-';
|
||
|
||
// Handle template format (language/framework)
|
||
if (componentName.includes('/')) {
|
||
const parts = componentName.split('/');
|
||
return parts.map(part =>
|
||
part.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase())
|
||
).join('/');
|
||
}
|
||
|
||
// Handle individual components
|
||
return componentName
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* Format relative time (e.g., "2 hours ago")
|
||
*/
|
||
function formatRelativeTime(date) {
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffMins < 60) {
|
||
return diffMins <= 1 ? 'just now' : `${diffMins} minutes ago`;
|
||
} else if (diffHours < 24) {
|
||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||
} else if (diffDays < 7) {
|
||
return diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
|
||
} else {
|
||
return date.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refresh statistics data
|
||
*/
|
||
async function refreshStatistics() {
|
||
const refreshButton = document.querySelector('.stats-refresh-btn');
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refreshing...';
|
||
refreshButton.disabled = true;
|
||
}
|
||
|
||
await loadDownloadStatistics();
|
||
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refresh';
|
||
refreshButton.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Auto-refresh statistics every 10 minutes
|
||
setInterval(loadDownloadStatistics, 10 * 60 * 1000);
|
||
|
||
// Close modal
|
||
function closeModal() {
|
||
const modal = document.querySelector('.modal');
|
||
if (modal) {
|
||
modal.remove();
|
||
}
|
||
}
|
||
|
||
// Show contribute modal
|
||
function showContributeModal() {
|
||
const modalHTML = `
|
||
<div class="modal-overlay" onclick="closeModal()">
|
||
<div class="modal-content contribute-modal" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<h3>🚀 Contribute a New Template</h3>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="contribute-intro">
|
||
<p>Help expand Claude Code Templates by contributing new languages or frameworks! Follow these steps to submit your contribution:</p>
|
||
</div>
|
||
|
||
<div class="contribute-steps">
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">1</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Fork the Repository</h4>
|
||
<p>Go to the <a href="https://github.com/davila7/claude-code-templates" target="_blank">main repository</a> and click "Fork" to create your own copy.</p>
|
||
<div class="step-command">
|
||
<code>git clone https://github.com/YOUR_USERNAME/claude-code-templates.git</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('git clone https://github.com/YOUR_USERNAME/claude-code-templates.git')">Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">2</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Choose Your Contribution Type</h4>
|
||
<div class="contribution-types">
|
||
<div class="contrib-type">
|
||
<strong>New Language:</strong> Add to <code>cli-tool/templates/</code>
|
||
<br><small>Example: <code>cli-tool/templates/kotlin/</code></small>
|
||
</div>
|
||
<div class="contrib-type">
|
||
<strong>New Framework:</strong> Add to <code>cli-tool/templates/{language}/examples/</code>
|
||
<br><small>Example: <code>cli-tool/templates/python/examples/fastapi-app/</code></small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">3</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Use Claude Code to Generate the Template</h4>
|
||
<p>Copy this prompt and use it with Claude Code to automatically generate the template structure:</p>
|
||
<div class="claude-prompt">
|
||
<h5>📋 Claude Code Prompt:</h5>
|
||
<div class="prompt-text">
|
||
<pre>I want to contribute a new template to the claude-code-templates repository.
|
||
|
||
Please help me create:
|
||
- A new [LANGUAGE/FRAMEWORK] template
|
||
- All necessary configuration files (CLAUDE.md, .claude/, .mcp.json)
|
||
- Update cli-tool/src/templates.js with the new configuration
|
||
- Include appropriate hooks and commands for this technology
|
||
|
||
The template should follow the existing patterns in the repository and include:
|
||
1. CLAUDE.md with language/framework-specific guidelines
|
||
2. .claude/commands/ with relevant development commands
|
||
3. .mcp.json with appropriate MCP configurations
|
||
4. Update templates.js with the new template definition
|
||
|
||
Target: [SPECIFY: New language (e.g., "Kotlin") OR new framework (e.g., "FastAPI for Python")]
|
||
|
||
Please analyze the existing templates in the repository first to understand the structure and patterns, then create the new template following the same conventions.</pre>
|
||
</div>
|
||
<button class="copy-btn copy-prompt-btn" onclick="copyToClipboard(document.querySelector('.prompt-text pre').textContent)">Copy Prompt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">4</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Test Your Template</h4>
|
||
<p>Before submitting, test your template locally:</p>
|
||
<div class="step-command">
|
||
<code>cd cli-tool && npm test</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('cd cli-tool && npm test')">Copy</button>
|
||
</div>
|
||
<div class="step-command">
|
||
<code>node src/index.js --language=YOUR_LANGUAGE</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('node src/index.js --language=YOUR_LANGUAGE')">Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">5</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Submit Pull Request</h4>
|
||
<p>Create a pull request with your changes:</p>
|
||
<div class="step-command">
|
||
<code>git add . && git commit -m "feat: Add [LANGUAGE/FRAMEWORK] template"</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('git add . && git commit -m \"feat: Add [LANGUAGE/FRAMEWORK] template\"')">Copy</button>
|
||
</div>
|
||
<div class="step-command">
|
||
<code>git push origin main</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('git push origin main')">Copy</button>
|
||
</div>
|
||
<p>Then go to GitHub and create a Pull Request with:</p>
|
||
<ul>
|
||
<li>Clear title: "feat: Add [Language/Framework] template"</li>
|
||
<li>Description of the template and its use cases</li>
|
||
<li>Screenshots or examples if applicable</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-footer">
|
||
<div class="help-section">
|
||
<h4>Need Help?</h4>
|
||
<p>Check out <a href="https://github.com/davila7/claude-code-templates/blob/main/CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a> or open an issue on GitHub.</p>
|
||
</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);
|
||
|
||
// Add event listener for ESC key
|
||
const handleEscape = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeComponentModal();
|
||
document.removeEventListener('keydown', handleEscape);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleEscape);
|
||
}
|
||
|
||
// ===== ANALYTICS AND STATISTICS FUNCTIONALITY =====
|
||
|
||
/**
|
||
* Load and display download statistics from GitHub-generated analytics
|
||
*/
|
||
async function loadDownloadStatistics() {
|
||
try {
|
||
// Fetch analytics data generated by GitHub Actions
|
||
const response = await fetch('analytics/download-stats.json?t=' + Date.now());
|
||
|
||
if (!response.ok) {
|
||
hideStatisticsSection();
|
||
return;
|
||
}
|
||
|
||
const analyticsData = await response.json();
|
||
displayDownloadStatistics(analyticsData);
|
||
|
||
} catch (error) {
|
||
hideStatisticsSection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display download statistics in the UI
|
||
*/
|
||
function displayDownloadStatistics(data) {
|
||
// Update total downloads
|
||
const totalElement = document.getElementById('totalDownloads');
|
||
if (totalElement) {
|
||
totalElement.textContent = formatNumber(data.total_downloads || 0);
|
||
}
|
||
|
||
// Update individual component type counts
|
||
const typeElements = {
|
||
agentDownloads: data.downloads_by_type?.agent || 0,
|
||
commandDownloads: data.downloads_by_type?.command || 0,
|
||
mcpDownloads: data.downloads_by_type?.mcp || 0,
|
||
templateDownloads: data.downloads_by_type?.template || 0
|
||
};
|
||
|
||
Object.entries(typeElements).forEach(([elementId, count]) => {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.textContent = formatNumber(count);
|
||
}
|
||
});
|
||
|
||
// Find and display most popular component
|
||
const popularElement = document.getElementById('popularComponent');
|
||
if (popularElement && data.downloads_by_component) {
|
||
const topComponent = Object.entries(data.downloads_by_component)[0];
|
||
if (topComponent) {
|
||
const [componentName, downloadCount] = topComponent;
|
||
popularElement.textContent = formatComponentNameForDisplay(componentName);
|
||
popularElement.setAttribute('title', `${formatNumber(downloadCount)} downloads`);
|
||
} else {
|
||
popularElement.textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Update last updated timestamp
|
||
const lastUpdatedElement = document.getElementById('statsLastUpdated');
|
||
if (lastUpdatedElement && data.last_updated) {
|
||
const lastUpdated = new Date(data.last_updated);
|
||
lastUpdatedElement.textContent = formatRelativeTime(lastUpdated);
|
||
}
|
||
|
||
// Show the statistics section
|
||
showStatisticsSection();
|
||
|
||
}
|
||
|
||
/**
|
||
* Hide statistics section when data is not available
|
||
*/
|
||
function hideStatisticsSection() {
|
||
// Keep statistics section visible even when data is not available
|
||
// const statsSection = document.getElementById('downloadStatsSection');
|
||
// if (statsSection) {
|
||
// statsSection.style.display = 'none';
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Show statistics section when data is available
|
||
* Note: Stats section has been moved to dedicated page at /download-stats.html
|
||
*/
|
||
function showStatisticsSection() {
|
||
// Stats section removed from main page - no action needed
|
||
}
|
||
|
||
/**
|
||
* Format numbers with thousands separators
|
||
*/
|
||
function formatNumber(num) {
|
||
if (num === 0) return '0';
|
||
if (num < 1000) return num.toString();
|
||
if (num < 1000000) return (num / 1000).toFixed(1) + 'K';
|
||
return (num / 1000000).toFixed(1) + 'M';
|
||
}
|
||
|
||
/**
|
||
* Format component name for display (remove prefixes, capitalize)
|
||
*/
|
||
function formatComponentNameForDisplay(componentName) {
|
||
if (!componentName || componentName === 'unknown') return '-';
|
||
|
||
// Handle template format (language/framework)
|
||
if (componentName.includes('/')) {
|
||
const parts = componentName.split('/');
|
||
return parts.map(part =>
|
||
part.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase())
|
||
).join('/');
|
||
}
|
||
|
||
// Handle individual components
|
||
return componentName
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* Format relative time (e.g., "2 hours ago")
|
||
*/
|
||
function formatRelativeTime(date) {
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffMins < 60) {
|
||
return diffMins <= 1 ? 'just now' : `${diffMins} minutes ago`;
|
||
} else if (diffHours < 24) {
|
||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||
} else if (diffDays < 7) {
|
||
return diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
|
||
} else {
|
||
return date.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refresh statistics data
|
||
*/
|
||
async function refreshStatistics() {
|
||
const refreshButton = document.querySelector('.stats-refresh-btn');
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refreshing...';
|
||
refreshButton.disabled = true;
|
||
}
|
||
|
||
await loadDownloadStatistics();
|
||
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refresh';
|
||
refreshButton.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Auto-refresh statistics every 10 minutes
|
||
setInterval(loadDownloadStatistics, 10 * 60 * 1000);
|
||
|
||
// Get file type based on extension/name
|
||
function getFileType(filename) {
|
||
if (filename.endsWith('.md')) return 'Documentation';
|
||
if (filename.endsWith('.json')) return 'Configuration';
|
||
if (filename.includes('.claude')) return 'Commands';
|
||
if (filename.includes('commands')) return 'Commands';
|
||
return 'Configuration';
|
||
}
|
||
|
||
// Generate GitHub folder URL for templates
|
||
function getGithubFolderUrl(languageKey, frameworkKey) {
|
||
const baseUrl = 'https://github.com/davila7/claude-code-templates/tree/main/cli-tool/templates';
|
||
|
||
if (frameworkKey === 'none' || !frameworkKey) {
|
||
// Base language template
|
||
return `${baseUrl}/${languageKey}`;
|
||
} else {
|
||
// Framework-specific template
|
||
return `${baseUrl}/${languageKey}/examples/${frameworkKey}-app`;
|
||
}
|
||
}
|
||
|
||
// Generate GitHub file URL for individual files
|
||
function getGithubFileUrl(languageKey, frameworkKey, filePath) {
|
||
const baseUrl = 'https://github.com/davila7/claude-code-templates/blob/main/cli-tool/templates';
|
||
return `${baseUrl}/${filePath}`;
|
||
}
|
||
|
||
// Copy to clipboard function
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
showCopyFeedback();
|
||
}).catch(err => {
|
||
console.error('Failed to copy: ', err);
|
||
// Fallback for older browsers
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
showCopyFeedback();
|
||
} catch (err) {
|
||
console.error('Fallback copy failed: ', err);
|
||
}
|
||
document.body.removeChild(textArea);
|
||
});
|
||
}
|
||
|
||
function showCopyFeedback() {
|
||
// Create temporary feedback element
|
||
const feedback = document.createElement('div');
|
||
feedback.textContent = 'Copied to clipboard!';
|
||
feedback.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #48bb78;
|
||
color: white;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
font-weight: 500;
|
||
z-index: 1000;
|
||
animation: slideInRight 0.3s ease;
|
||
`;
|
||
|
||
document.body.appendChild(feedback);
|
||
|
||
// Remove feedback after 2 seconds
|
||
setTimeout(() => {
|
||
feedback.style.animation = 'slideOutRight 0.3s ease';
|
||
setTimeout(() => {
|
||
document.body.removeChild(feedback);
|
||
}, 300);
|
||
}, 2000);
|
||
}
|
||
|
||
// Add CSS animations for feedback and error states
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideOutRight {
|
||
from {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
to {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.error-message {
|
||
grid-column: 1 / -1;
|
||
text-align: center;
|
||
color: white;
|
||
background: rgba(220, 53, 69, 0.2);
|
||
border: 1px solid rgba(220, 53, 69, 0.3);
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.error-message h3 {
|
||
margin-bottom: 1rem;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.retry-btn {
|
||
background: #dc3545;
|
||
color: white;
|
||
border: none;
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
margin-top: 1rem;
|
||
font-size: 0.9rem;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.retry-btn:hover {
|
||
background: #c82333;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// Initialize the page
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Start with agents view (new default)
|
||
setUnifiedFilter('agents');
|
||
// Load download statistics
|
||
loadDownloadStatistics();
|
||
});
|
||
|
||
// Add keyboard navigation
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
// Close all flipped cards
|
||
document.querySelectorAll('.template-card.flipped').forEach(card => {
|
||
card.classList.remove('flipped');
|
||
});
|
||
}
|
||
});
|
||
|
||
// Auto-refresh templates every 5 minutes to pick up changes
|
||
setInterval(fetchTemplatesConfig, 5 * 60 * 1000);
|
||
|
||
// ===== UNIFIED COMPONENTS FUNCTIONALITY =====
|
||
|
||
let componentsData = {
|
||
agents: [],
|
||
commands: [],
|
||
mcps: []
|
||
};
|
||
|
||
let currentFilter = 'skills';
|
||
let currentCategoryFilter = 'all';
|
||
let allDataLoaded = false;
|
||
let downloadStats = null;
|
||
let availableCategories = {
|
||
agents: new Set(),
|
||
commands: new Set(),
|
||
mcps: new Set()
|
||
};
|
||
|
||
// Unified filter functionality
|
||
function setUnifiedFilter(filter) {
|
||
currentFilter = filter;
|
||
currentCategoryFilter = 'all'; // Reset category filter when changing main filter
|
||
|
||
// Update filter buttons
|
||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
const targetFilterBtn = document.querySelector(`[data-filter="${filter}"]`);
|
||
if (targetFilterBtn) {
|
||
targetFilterBtn.classList.add('active');
|
||
}
|
||
|
||
// Load and display content
|
||
if (filter === 'templates') {
|
||
displayTemplates();
|
||
} else {
|
||
loadAndDisplayComponents();
|
||
}
|
||
}
|
||
|
||
// Set category filter
|
||
function setCategoryFilter(category) {
|
||
currentCategoryFilter = 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
|
||
generateUnifiedComponentCards();
|
||
}
|
||
|
||
// Make setCategoryFilter available globally
|
||
window.setCategoryFilter = setCategoryFilter;
|
||
|
||
// Display templates (existing functionality)
|
||
function displayTemplates() {
|
||
const unifiedGrid = document.getElementById('unifiedGrid');
|
||
unifiedGrid.className = 'unified-grid templates-mode';
|
||
|
||
if (templatesData) {
|
||
generateTemplateCards();
|
||
} else {
|
||
unifiedGrid.innerHTML = '<div class="loading">Loading templates from GitHub...</div>';
|
||
fetchTemplatesConfig();
|
||
}
|
||
}
|
||
|
||
// Load and display components
|
||
async function loadAndDisplayComponents() {
|
||
const unifiedGrid = document.getElementById('unifiedGrid');
|
||
unifiedGrid.className = 'unified-grid components-mode';
|
||
|
||
if (!allDataLoaded) {
|
||
unifiedGrid.innerHTML = '<div class="loading">Loading components from GitHub...</div>';
|
||
await loadAllComponentsData();
|
||
}
|
||
|
||
generateUnifiedComponentCards();
|
||
}
|
||
|
||
// Load all components data
|
||
async function loadAllComponentsData() {
|
||
try {
|
||
const response = await fetch('components.json');
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
const data = await response.json();
|
||
componentsData = data;
|
||
collectAvailableCategories();
|
||
allDataLoaded = true;
|
||
} catch (error) {
|
||
console.error('Error loading components:', error);
|
||
const unifiedGrid = document.getElementById('unifiedGrid');
|
||
unifiedGrid.innerHTML = `
|
||
<div class="error-message">
|
||
<h3>Error loading components</h3>
|
||
<p>Could not fetch components from local data. Please try again later.</p>
|
||
<button onclick="loadAndDisplayComponents()" class="retry-btn">Retry</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Update category sub-filters in the unified-filter-bar
|
||
function updateCategorySubFilters() {
|
||
const unifiedFilterBar = document.querySelector('.unified-filter-bar');
|
||
|
||
// 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(availableCategories[currentFilter] || []).sort();
|
||
|
||
if (currentCategories.length <= 1 || currentFilter === 'templates') {
|
||
// Don't show sub-filters if there's only one category, none, or templates
|
||
return;
|
||
}
|
||
|
||
// Create category filter row
|
||
const categoryFilterRow = document.createElement('div');
|
||
categoryFilterRow.className = 'category-filter-row';
|
||
categoryFilterRow.innerHTML = `
|
||
<div class="category-filter-label">Categories:</div>
|
||
<div class="category-filter-buttons">
|
||
<button class="category-filter-btn ${currentCategoryFilter === 'all' ? 'active' : ''}"
|
||
data-category="all">
|
||
All
|
||
</button>
|
||
${currentCategories.map(category => `
|
||
<button class="category-filter-btn ${currentCategoryFilter === category ? 'active' : ''}"
|
||
data-category="${category}">
|
||
${formatComponentName(category)}
|
||
</button>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
// Add click event listeners
|
||
categoryFilterRow.querySelectorAll('.category-filter-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
setCategoryFilter(btn.getAttribute('data-category'));
|
||
});
|
||
});
|
||
|
||
// Append to unified filter bar
|
||
unifiedFilterBar.appendChild(categoryFilterRow);
|
||
}
|
||
|
||
// Generate unified component cards
|
||
function generateUnifiedComponentCards() {
|
||
const unifiedGrid = document.getElementById('unifiedGrid');
|
||
unifiedGrid.innerHTML = '';
|
||
|
||
// Update category sub-filters in the unified-filter-bar
|
||
updateCategorySubFilters();
|
||
|
||
// Get filtered components
|
||
const filteredComponents = getFilteredComponents();
|
||
|
||
// Add "Add New" card based on current filter
|
||
if (currentFilter !== 'templates') {
|
||
const addCard = createAddComponentCard(currentFilter);
|
||
unifiedGrid.appendChild(addCard);
|
||
}
|
||
|
||
// Add component cards
|
||
filteredComponents.forEach(component => {
|
||
const card = createComponentCard(component);
|
||
unifiedGrid.appendChild(card);
|
||
});
|
||
|
||
// Update filter button with count
|
||
updateFilterCount();
|
||
}
|
||
|
||
// Update filter button count
|
||
function updateFilterCount() {
|
||
const filterBtn = document.querySelector(`[data-filter="${currentFilter}"]`);
|
||
if (filterBtn && currentFilter !== 'templates') {
|
||
const components = componentsData[currentFilter];
|
||
if (components && Array.isArray(components)) {
|
||
const count = components.length;
|
||
const originalText = filterBtn.textContent.split('(')[0].trim();
|
||
filterBtn.textContent = `${originalText} (${count})`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Get filtered components based on current filter and category filter
|
||
function getFilteredComponents() {
|
||
if (currentFilter === 'templates') {
|
||
return [];
|
||
}
|
||
|
||
let components = componentsData[currentFilter] || [];
|
||
|
||
// Apply category filter if not 'all'
|
||
if (currentCategoryFilter !== 'all') {
|
||
components = components.filter(component => {
|
||
const category = component.category || 'general';
|
||
return category === currentCategoryFilter;
|
||
});
|
||
}
|
||
|
||
return components;
|
||
}
|
||
|
||
// Collect available categories from loaded components
|
||
function collectAvailableCategories() {
|
||
// Reset categories
|
||
availableCategories.agents.clear();
|
||
availableCategories.commands.clear();
|
||
availableCategories.mcps.clear();
|
||
|
||
// Collect categories from each component type
|
||
if (componentsData.agents && Array.isArray(componentsData.agents)) {
|
||
componentsData.agents.forEach(component => {
|
||
const category = component.category || 'general';
|
||
availableCategories.agents.add(category);
|
||
});
|
||
}
|
||
|
||
if (componentsData.commands && Array.isArray(componentsData.commands)) {
|
||
componentsData.commands.forEach(component => {
|
||
const category = component.category || 'general';
|
||
availableCategories.commands.add(category);
|
||
});
|
||
}
|
||
|
||
if (componentsData.mcps && Array.isArray(componentsData.mcps)) {
|
||
componentsData.mcps.forEach(component => {
|
||
const category = component.category || 'general';
|
||
availableCategories.mcps.add(category);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Load components data from GitHub (legacy function for compatibility)
|
||
async function loadComponentsData() {
|
||
// Redirect to unified loading function
|
||
await loadAndDisplayComponents();
|
||
}
|
||
|
||
// Load specific component type from GitHub
|
||
async function loadComponentType(type) {
|
||
const baseUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/cli-tool/components/${type}`;
|
||
|
||
try {
|
||
const response = await fetch(baseUrl);
|
||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||
|
||
const files = await response.json();
|
||
const componentPromises = files.map(async (file) => {
|
||
if (file.name.endsWith('.md') || file.name.endsWith('.json')) {
|
||
// Direct file in root level
|
||
const contentResponse = await fetch(file.download_url);
|
||
const content = await contentResponse.text();
|
||
|
||
return {
|
||
name: file.name.replace(/\.(md|json)$/, ''),
|
||
type: type,
|
||
filename: file.name,
|
||
content: content,
|
||
url: file.html_url,
|
||
category: null // No category for root level components
|
||
};
|
||
} else if (file.type === 'dir') {
|
||
// Handle subdirectories for all component types (categories)
|
||
try {
|
||
const categoryResponse = await fetch(`https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/cli-tool/components/${type}/${file.name}`);
|
||
if (categoryResponse.ok) {
|
||
const categoryFiles = await categoryResponse.json();
|
||
const categoryComponentPromises = categoryFiles.map(async (categoryFile) => {
|
||
if (categoryFile.name.endsWith('.md') || categoryFile.name.endsWith('.json')) {
|
||
const contentResponse = await fetch(categoryFile.download_url);
|
||
const content = await contentResponse.text();
|
||
|
||
return {
|
||
name: `${file.name}/${categoryFile.name.replace(/\.(md|json)$/, '')}`,
|
||
type: type,
|
||
filename: categoryFile.name,
|
||
content: content,
|
||
url: categoryFile.html_url,
|
||
category: file.name
|
||
};
|
||
}
|
||
return null;
|
||
});
|
||
|
||
const categoryComponents = await Promise.all(categoryComponentPromises);
|
||
return categoryComponents.filter(c => c !== null);
|
||
}
|
||
} catch (error) {
|
||
console.warn(`Warning: Could not load category ${file.name}:`, error);
|
||
return [];
|
||
}
|
||
}
|
||
return null;
|
||
});
|
||
|
||
const componentsNested = await Promise.all(componentPromises);
|
||
// Flatten the array since subdirectories return arrays
|
||
const components = componentsNested.flat().filter(c => c !== null);
|
||
return components;
|
||
|
||
} catch (error) {
|
||
console.error(`Error loading ${type}:`, error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Generate component cards with filter functionality
|
||
function generateComponentCards() {
|
||
const componentsGrid = document.getElementById('componentsGrid');
|
||
|
||
// Create filter bar
|
||
const filterBar = createComponentFilterBar();
|
||
|
||
// Create components container
|
||
const componentsContainer = document.createElement('div');
|
||
componentsContainer.className = 'components-container';
|
||
|
||
// Create Add New cards for each category
|
||
const addAgentCard = createAddComponentCard('agents');
|
||
const addCommandCard = createAddComponentCard('commands');
|
||
const addMcpCard = createAddComponentCard('mcps');
|
||
|
||
// Filter and display components
|
||
const filteredComponents = getFilteredComponents();
|
||
|
||
componentsContainer.innerHTML = '';
|
||
|
||
// Add "Add New" cards based on filter
|
||
if (currentFilter === 'all' || currentFilter === 'agents') {
|
||
componentsContainer.appendChild(addAgentCard);
|
||
}
|
||
if (currentFilter === 'all' || currentFilter === 'commands') {
|
||
componentsContainer.appendChild(addCommandCard);
|
||
}
|
||
if (currentFilter === 'all' || currentFilter === 'mcps') {
|
||
componentsContainer.appendChild(addMcpCard);
|
||
}
|
||
|
||
// Add component cards
|
||
filteredComponents.forEach(component => {
|
||
const card = createComponentCard(component);
|
||
componentsContainer.appendChild(card);
|
||
});
|
||
|
||
componentsGrid.innerHTML = '';
|
||
componentsGrid.appendChild(filterBar);
|
||
componentsGrid.appendChild(componentsContainer);
|
||
}
|
||
|
||
// Create filter bar for components
|
||
function createComponentFilterBar() {
|
||
const filterBar = document.createElement('div');
|
||
filterBar.className = 'component-filter-bar';
|
||
|
||
const totalCounts = {
|
||
all: componentsData.agents.length + componentsData.commands.length + componentsData.mcps.length,
|
||
agents: componentsData.agents.length,
|
||
commands: componentsData.commands.length,
|
||
mcps: componentsData.mcps.length
|
||
};
|
||
|
||
filterBar.innerHTML = `
|
||
<div class="filter-label">$ Filter Components</div>
|
||
<div class="filter-buttons">
|
||
<button class="filter-btn ${currentFilter === 'all' ? 'active' : ''}" onclick="setComponentFilter('all')">
|
||
All (${totalCounts.all})
|
||
</button>
|
||
<button class="filter-btn ${currentFilter === 'agents' ? 'active' : ''}" onclick="setComponentFilter('agents')">
|
||
🤖 Agents (${totalCounts.agents})
|
||
</button>
|
||
<button class="filter-btn ${currentFilter === 'commands' ? 'active' : ''}" onclick="setComponentFilter('commands')">
|
||
⚡ Commands (${totalCounts.commands})
|
||
</button>
|
||
<button class="filter-btn ${currentFilter === 'mcps' ? 'active' : ''}" onclick="setComponentFilter('mcps')">
|
||
🔌 MCPs (${totalCounts.mcps})
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
return filterBar;
|
||
}
|
||
|
||
// Set component filter
|
||
function setComponentFilter(filter) {
|
||
currentFilter = filter;
|
||
generateComponentCards();
|
||
}
|
||
|
||
|
||
// Create Add Component card
|
||
function createAddComponentCard(type) {
|
||
const card = document.createElement('div');
|
||
card.className = 'template-card add-template-card add-component-card';
|
||
|
||
const typeConfig = {
|
||
agents: {
|
||
icon: `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
||
</svg>`,
|
||
name: 'Agent',
|
||
description: 'Create a new AI specialist agent'
|
||
},
|
||
commands: {
|
||
icon: `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
||
</svg>`,
|
||
name: 'Command',
|
||
description: 'Add a custom slash command'
|
||
},
|
||
mcps: {
|
||
icon: `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
||
</svg>`,
|
||
name: 'MCP',
|
||
description: 'Build a Model Context Protocol integration'
|
||
}
|
||
};
|
||
|
||
const config = typeConfig[type];
|
||
|
||
card.innerHTML = `
|
||
<div class="card-inner">
|
||
<div class="card-front">
|
||
<div class="framework-logo">
|
||
${config.icon}
|
||
</div>
|
||
<h3 class="template-title">Add New ${config.name}</h3>
|
||
<p class="template-description">${config.description}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add click handler to open contribution modal directly (no flip)
|
||
card.addEventListener('click', () => {
|
||
showComponentContributeModal(type);
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// Create individual component card
|
||
function createComponentCard(component) {
|
||
const card = document.createElement('div');
|
||
card.className = 'template-card';
|
||
|
||
const typeConfig = {
|
||
agent: { icon: '🤖', color: '#ff6b6b' },
|
||
command: { icon: '⚡', color: '#4ecdc4' },
|
||
mcp: { icon: '🔌', color: '#45b7d1' }
|
||
};
|
||
|
||
const config = typeConfig[component.type];
|
||
const installCommand = generateInstallCommand(component);
|
||
|
||
// Get download count for this component
|
||
const downloadCount = getDownloadCount(component.name, component.type);
|
||
const downloadBadge = createDownloadBadge(downloadCount);
|
||
|
||
// Get validation badge
|
||
const validationBadge = createValidationBadge(component.security);
|
||
|
||
// Create category label for all components (use "General" if no category)
|
||
const categoryName = component.category || 'general';
|
||
const categoryLabel = `<div class="category-label">${formatComponentName(categoryName)}</div>`;
|
||
|
||
card.innerHTML = `
|
||
<div class="card-inner">
|
||
<div class="card-front">
|
||
${downloadBadge}
|
||
${validationBadge}
|
||
${categoryLabel}
|
||
<div class="framework-logo" style="color: ${config.color}">
|
||
<span class="component-icon">${config.icon}</span>
|
||
</div>
|
||
<h3 class="template-title">${formatComponentName(component.name)}</h3>
|
||
<p class="template-description">${getComponentDescription(component)}</p>
|
||
</div>
|
||
<div class="card-back">
|
||
<div class="command-display">
|
||
<h3>Installation Command</h3>
|
||
<div class="command-code">${installCommand}</div>
|
||
<div class="action-buttons">
|
||
<button class="view-files-btn" onclick="showComponentDetails('${component.type}', '${component.name}')">
|
||
📁 View Details
|
||
</button>
|
||
<button class="copy-command-btn" onclick="copyToClipboard('${installCommand}')">
|
||
📋 Copy Command
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add click handler for card flip
|
||
card.addEventListener('click', (e) => {
|
||
if (!e.target.closest('button')) {
|
||
card.classList.toggle('flipped');
|
||
}
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// Generate install command for component
|
||
function generateInstallCommand(component) {
|
||
if (component.type === 'agent') {
|
||
return `npx claude-code-templates@latest --agent=${component.name} --yes`;
|
||
} else if (component.type === 'command') {
|
||
return `npx claude-code-templates@latest --command=${component.name} --yes`;
|
||
} else if (component.type === 'mcp') {
|
||
// Remove .json extension from MCP names for the command
|
||
const mcpName = component.name.replace(/\.json$/, '');
|
||
return `npx claude-code-templates@latest --mcp=${mcpName} --yes`;
|
||
}
|
||
return `npx claude-code-templates@latest`;
|
||
}
|
||
|
||
|
||
// Get installation notes (removed to match template cards design)
|
||
function getInstallationNotes() {
|
||
return '';
|
||
}
|
||
|
||
// Format component name for display
|
||
function formatComponentName(name) {
|
||
// Handle subcategorized agents (e.g., "deep-research-team/academic-researcher")
|
||
if (name.includes('/')) {
|
||
const parts = name.split('/');
|
||
const actualName = parts[parts.length - 1]; // Get the last part after the slash
|
||
return actualName
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
return name
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
// Get component description
|
||
function getComponentDescription(component) {
|
||
return `A component of type '${component.type}' for Claude Code.`;
|
||
}
|
||
|
||
// Show component details modal
|
||
async function showComponentDetails(type, name) {
|
||
const component = componentsData[type + 's'].find(c => c.name === name);
|
||
if (!component) return;
|
||
|
||
const modal = document.getElementById('componentModal');
|
||
document.getElementById('componentModalTitle').textContent = name;
|
||
document.getElementById('componentModalType').textContent = type;
|
||
document.getElementById('componentModalCategory').textContent = component.category;
|
||
document.getElementById('componentModalPath').textContent = component.path;
|
||
document.getElementById('componentModalUsage').textContent = `cct --${type} "${component.path}"`;
|
||
|
||
const descriptionElement = document.getElementById('componentModalDescription');
|
||
descriptionElement.textContent = component.content || 'No content available.';
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
// Show component contribute modal
|
||
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'
|
||
}
|
||
};
|
||
|
||
const config = typeConfig[type];
|
||
|
||
const modalHTML = `
|
||
<div class="modal-overlay" onclick="closeModal()">
|
||
<div class="modal-content contribute-modal" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<h3>📝 Contribute a New ${config.name}</h3>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="contribute-intro">
|
||
<p>Help expand Claude Code by contributing a new ${config.name.toLowerCase()}! Follow these steps:</p>
|
||
</div>
|
||
|
||
<div class="contribute-steps">
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">1</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Create Your ${config.name}</h4>
|
||
<p>Add your ${config.name.toLowerCase()} to: <code>cli-tool/components/${type}/</code></p>
|
||
<div class="component-structure">
|
||
<strong>Structure should include:</strong>
|
||
<pre>${config.structure}</pre>
|
||
</div>
|
||
<div class="step-command">
|
||
<strong>Example filename:</strong> <code>${config.example}.${type === 'mcps' ? 'json' : 'md'}</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">2</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Follow the Pattern</h4>
|
||
<p>Check existing ${type} in the repository to understand the structure and conventions.</p>
|
||
<div class="step-command">
|
||
<a href="https://github.com/davila7/claude-code-templates/tree/main/cli-tool/components/${type}" target="_blank" class="github-folder-link">
|
||
📁 View existing ${type}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">3</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Test Your Component</h4>
|
||
<p>Ensure your ${config.name.toLowerCase()} works correctly with Claude Code.</p>
|
||
<div class="step-command">
|
||
<code>cd cli-tool && npm test</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('cd cli-tool && npm test')">Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-step">
|
||
<div class="step-number-contrib">4</div>
|
||
<div class="step-content-contrib">
|
||
<h4>Submit Pull Request</h4>
|
||
<p>Submit your contribution with proper documentation:</p>
|
||
<div class="step-command">
|
||
<code>git add cli-tool/components/${type}/${config.example}.${type === 'mcps' ? 'json' : 'md'}</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('git add cli-tool/components/${type}/${config.example}.${type === 'mcps' ? 'json' : 'md'}')">Copy</button>
|
||
</div>
|
||
<div class="step-command">
|
||
<code>git commit -m "feat: Add ${config.example} ${config.name.toLowerCase()}"</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('git commit -m \"feat: Add ${config.example} ${config.name.toLowerCase()}\"')">Copy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contribute-footer">
|
||
<div class="help-section">
|
||
<h4>Need Help?</h4>
|
||
<p>Check existing ${type} for examples or open an issue on GitHub for guidance.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add modal to page
|
||
const modal = document.createElement('div');
|
||
modal.innerHTML = modalHTML;
|
||
modal.className = 'modal contribute-component-modal';
|
||
document.body.appendChild(modal);
|
||
|
||
// Add event listener for ESC key
|
||
const handleEscape = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeComponentModal();
|
||
document.removeEventListener('keydown', handleEscape);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleEscape);
|
||
}
|
||
|
||
// ===== ANALYTICS AND STATISTICS FUNCTIONALITY =====
|
||
|
||
/**
|
||
* Load and display download statistics from GitHub-generated analytics
|
||
*/
|
||
async function loadDownloadStatistics() {
|
||
try {
|
||
// Fetch analytics data generated by GitHub Actions
|
||
const response = await fetch('analytics/download-stats.json?t=' + Date.now());
|
||
|
||
if (!response.ok) {
|
||
hideStatisticsSection();
|
||
return;
|
||
}
|
||
|
||
const analyticsData = await response.json();
|
||
displayDownloadStatistics(analyticsData);
|
||
|
||
} catch (error) {
|
||
hideStatisticsSection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display download statistics in the UI
|
||
*/
|
||
function displayDownloadStatistics(data) {
|
||
// Update total downloads
|
||
const totalElement = document.getElementById('totalDownloads');
|
||
if (totalElement) {
|
||
totalElement.textContent = formatNumber(data.total_downloads || 0);
|
||
}
|
||
|
||
// Update individual component type counts
|
||
const typeElements = {
|
||
agentDownloads: data.downloads_by_type?.agent || 0,
|
||
commandDownloads: data.downloads_by_type?.command || 0,
|
||
mcpDownloads: data.downloads_by_type?.mcp || 0,
|
||
templateDownloads: data.downloads_by_type?.template || 0
|
||
};
|
||
|
||
Object.entries(typeElements).forEach(([elementId, count]) => {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.textContent = formatNumber(count);
|
||
}
|
||
});
|
||
|
||
// Find and display most popular component
|
||
const popularElement = document.getElementById('popularComponent');
|
||
if (popularElement && data.downloads_by_component) {
|
||
const topComponent = Object.entries(data.downloads_by_component)[0];
|
||
if (topComponent) {
|
||
const [componentName, downloadCount] = topComponent;
|
||
popularElement.textContent = formatComponentNameForDisplay(componentName);
|
||
popularElement.setAttribute('title', `${formatNumber(downloadCount)} downloads`);
|
||
} else {
|
||
popularElement.textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Update last updated timestamp
|
||
const lastUpdatedElement = document.getElementById('statsLastUpdated');
|
||
if (lastUpdatedElement && data.last_updated) {
|
||
const lastUpdated = new Date(data.last_updated);
|
||
lastUpdatedElement.textContent = formatRelativeTime(lastUpdated);
|
||
}
|
||
|
||
// Show the statistics section
|
||
showStatisticsSection();
|
||
|
||
}
|
||
|
||
/**
|
||
* Hide statistics section when data is not available
|
||
*/
|
||
function hideStatisticsSection() {
|
||
// Keep statistics section visible even when data is not available
|
||
// const statsSection = document.getElementById('downloadStatsSection');
|
||
// if (statsSection) {
|
||
// statsSection.style.display = 'none';
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Show statistics section when data is available
|
||
* Note: Stats section has been moved to dedicated page at /download-stats.html
|
||
*/
|
||
function showStatisticsSection() {
|
||
// Stats section removed from main page - no action needed
|
||
}
|
||
|
||
/**
|
||
* Format numbers with thousands separators
|
||
*/
|
||
function formatNumber(num) {
|
||
if (num === 0) return '0';
|
||
if (num < 1000) return num.toString();
|
||
if (num < 1000000) return (num / 1000).toFixed(1) + 'K';
|
||
return (num / 1000000).toFixed(1) + 'M';
|
||
}
|
||
|
||
/**
|
||
* Format component name for display (remove prefixes, capitalize)
|
||
*/
|
||
function formatComponentNameForDisplay(componentName) {
|
||
if (!componentName || componentName === 'unknown') return '-';
|
||
|
||
// Handle template format (language/framework)
|
||
if (componentName.includes('/')) {
|
||
const parts = componentName.split('/');
|
||
return parts.map(part =>
|
||
part.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase())
|
||
).join('/');
|
||
}
|
||
|
||
// Handle individual components
|
||
return componentName
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* Format relative time (e.g., "2 hours ago")
|
||
*/
|
||
function formatRelativeTime(date) {
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffMins < 60) {
|
||
return diffMins <= 1 ? 'just now' : `${diffMins} minutes ago`;
|
||
} else if (diffHours < 24) {
|
||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||
} else if (diffDays < 7) {
|
||
return diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
|
||
} else {
|
||
return date.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refresh statistics data
|
||
*/
|
||
async function refreshStatistics() {
|
||
const refreshButton = document.querySelector('.stats-refresh-btn');
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refreshing...';
|
||
refreshButton.disabled = true;
|
||
}
|
||
|
||
await loadDownloadStatistics();
|
||
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refresh';
|
||
refreshButton.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Auto-refresh statistics every 10 minutes
|
||
setInterval(loadDownloadStatistics, 10 * 60 * 1000);
|
||
|
||
// Show detailed component modal
|
||
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) : 'No content available'}${component.content && component.content.length > 500 ? '...' : ''}</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<a href="${component.url}" 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>
|
||
</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);
|
||
|
||
// Add event listener for ESC key
|
||
const handleEscape = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeComponentModal();
|
||
document.removeEventListener('keydown', handleEscape);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleEscape);
|
||
}
|
||
|
||
// ===== ANALYTICS AND STATISTICS FUNCTIONALITY =====
|
||
|
||
/**
|
||
* Load and display download statistics from GitHub-generated analytics
|
||
*/
|
||
async function loadDownloadStatistics() {
|
||
try {
|
||
// Fetch analytics data generated by GitHub Actions
|
||
const response = await fetch('analytics/download-stats.json?t=' + Date.now());
|
||
|
||
if (!response.ok) {
|
||
hideStatisticsSection();
|
||
return;
|
||
}
|
||
|
||
const analyticsData = await response.json();
|
||
displayDownloadStatistics(analyticsData);
|
||
|
||
} catch (error) {
|
||
hideStatisticsSection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display download statistics in the UI
|
||
*/
|
||
function displayDownloadStatistics(data) {
|
||
// Update total downloads
|
||
// Store download stats globally for use in cards
|
||
downloadStats = data;
|
||
const totalElement = document.getElementById('totalDownloads');
|
||
if (totalElement) {
|
||
totalElement.textContent = formatNumber(data.total_downloads || 0);
|
||
}
|
||
|
||
// Update individual component type counts
|
||
const typeElements = {
|
||
agentDownloads: data.downloads_by_type?.agent || 0,
|
||
commandDownloads: data.downloads_by_type?.command || 0,
|
||
mcpDownloads: data.downloads_by_type?.mcp || 0,
|
||
templateDownloads: data.downloads_by_type?.template || 0
|
||
};
|
||
|
||
Object.entries(typeElements).forEach(([elementId, count]) => {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.textContent = formatNumber(count);
|
||
}
|
||
});
|
||
|
||
// Find and display most popular component
|
||
const popularElement = document.getElementById('popularComponent');
|
||
if (popularElement && data.downloads_by_component) {
|
||
const topComponent = Object.entries(data.downloads_by_component)[0];
|
||
if (topComponent) {
|
||
const [componentName, downloadCount] = topComponent;
|
||
popularElement.textContent = formatComponentNameForDisplay(componentName);
|
||
popularElement.setAttribute('title', `${formatNumber(downloadCount)} downloads`);
|
||
} else {
|
||
popularElement.textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Update last updated timestamp
|
||
const lastUpdatedElement = document.getElementById('statsLastUpdated');
|
||
if (lastUpdatedElement && data.last_updated) {
|
||
const lastUpdated = new Date(data.last_updated);
|
||
lastUpdatedElement.textContent = formatRelativeTime(lastUpdated);
|
||
}
|
||
|
||
// Show the statistics section
|
||
showStatisticsSection();
|
||
|
||
// Regenerate cards with download counts
|
||
if (currentFilter === 'templates' && templatesData) {
|
||
generateTemplateCards();
|
||
} else if (allDataLoaded) {
|
||
generateUnifiedComponentCards();
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Hide statistics section when data is not available
|
||
*/
|
||
function hideStatisticsSection() {
|
||
// Keep statistics section visible even when data is not available
|
||
// const statsSection = document.getElementById('downloadStatsSection');
|
||
// if (statsSection) {
|
||
// statsSection.style.display = 'none';
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* Show statistics section when data is available
|
||
* Note: Stats section has been moved to dedicated page at /download-stats.html
|
||
*/
|
||
function showStatisticsSection() {
|
||
// Stats section removed from main page - no action needed
|
||
}
|
||
|
||
/**
|
||
* Format numbers with thousands separators
|
||
*/
|
||
function formatNumber(num) {
|
||
if (num === 0) return '0';
|
||
if (num < 1000) return num.toString();
|
||
if (num < 1000000) return (num / 1000).toFixed(1) + 'K';
|
||
return (num / 1000000).toFixed(1) + 'M';
|
||
}
|
||
|
||
/**
|
||
* Format component name for display (remove prefixes, capitalize)
|
||
*/
|
||
function formatComponentNameForDisplay(componentName) {
|
||
if (!componentName || componentName === 'unknown') return '-';
|
||
|
||
// Handle template format (language/framework)
|
||
if (componentName.includes('/')) {
|
||
const parts = componentName.split('/');
|
||
return parts.map(part =>
|
||
part.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase())
|
||
).join('/');
|
||
}
|
||
|
||
// Handle individual components
|
||
return componentName
|
||
.replace(/-/g, ' ')
|
||
.replace(/\b\w/g, l => l.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* Format relative time (e.g., "2 hours ago")
|
||
*/
|
||
function formatRelativeTime(date) {
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffMins < 60) {
|
||
return diffMins <= 1 ? 'just now' : `${diffMins} minutes ago`;
|
||
} else if (diffHours < 24) {
|
||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||
} else if (diffDays < 7) {
|
||
return diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
|
||
} else {
|
||
return date.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refresh statistics data
|
||
*/
|
||
async function refreshStatistics() {
|
||
const refreshButton = document.querySelector('.stats-refresh-btn');
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refreshing...';
|
||
refreshButton.disabled = true;
|
||
}
|
||
|
||
await loadDownloadStatistics();
|
||
|
||
if (refreshButton) {
|
||
refreshButton.textContent = 'Refresh';
|
||
refreshButton.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Auto-refresh statistics every 10 minutes
|
||
setInterval(loadDownloadStatistics, 10 * 60 * 1000);
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const modal = document.getElementById('componentModal');
|
||
const closeBtn = document.getElementById('closeComponentModal');
|
||
const closeBtnFooter = document.getElementById('closeComponentModalBtn');
|
||
|
||
if (modal && closeBtn && closeBtnFooter) {
|
||
const closeModal = () => {
|
||
modal.style.display = 'none';
|
||
};
|
||
|
||
closeBtn.addEventListener('click', closeModal);
|
||
closeBtnFooter.addEventListener('click', closeModal);
|
||
|
||
window.addEventListener('click', (event) => {
|
||
if (event.target === modal) {
|
||
closeComponentModal();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') {
|
||
closeComponentModal();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Get download count for a specific component
|
||
*/
|
||
function getDownloadCount(componentName, componentType) {
|
||
if (!downloadStats || !downloadStats.downloads_by_component) {
|
||
return 0;
|
||
}
|
||
|
||
// For templates, the name format is "language/framework"
|
||
if (componentType === 'template') {
|
||
return downloadStats.downloads_by_component[componentName] || 0;
|
||
}
|
||
|
||
// For individual components (agents, commands, mcps)
|
||
return downloadStats.downloads_by_component[componentName] || 0;
|
||
}
|
||
|
||
/**
|
||
* Create download count badge HTML
|
||
*/
|
||
function createDownloadBadge(count) {
|
||
if (count === 0) return '';
|
||
|
||
return `<div class="download-badge" title="${count} downloads">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z"/>
|
||
</svg>
|
||
${formatNumber(count)}
|
||
</div>`;
|
||
}
|
||
|
||
/**
|
||
* Create validation badge HTML
|
||
*/
|
||
function createValidationBadge(validation) {
|
||
if (!validation || !validation.validated) return '';
|
||
|
||
const score = validation.score || 0;
|
||
const isValid = validation.valid;
|
||
|
||
// Perfect score (100%) - Show Twitter-style verified badge
|
||
if (score === 100 && isValid) {
|
||
return `<div class="validation-badge verified-badge" title="100% Validated - Perfect Security Score">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#1DA1F2">
|
||
<path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"/>
|
||
</svg>
|
||
<span class="verified-text">Verified</span>
|
||
</div>`;
|
||
}
|
||
|
||
// Determine color based on score
|
||
let badgeColor = '#48bb78'; // Green
|
||
let statusIcon = '✓';
|
||
|
||
if (!isValid || score < 70) {
|
||
badgeColor = '#f56565'; // Red
|
||
statusIcon = '✗';
|
||
} else if (score < 85) {
|
||
badgeColor = '#ed8936'; // Orange
|
||
statusIcon = '⚠';
|
||
}
|
||
|
||
return `<div class="validation-badge" style="background-color: ${badgeColor}" title="Quality Score: ${score}/100 - Validated">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||
</svg>
|
||
<span>${statusIcon} ${score}</span>
|
||
</div>`;
|
||
}
|