Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

794 lines
32 KiB
JavaScript

/**
* Trending Page JavaScript
* GitHub-inspired trending components page for Claude Code Templates
*/
class TrendingPage {
constructor() {
this.currentType = 'agents';
this.currentRange = 'month'; // Changed from 'today' to 'month' to show real data
this.data = null;
this.init();
}
async init() {
try {
await this.loadData();
this.setupEventListeners();
this.renderHeroStats();
this.renderPopularItems();
this.renderTopCountries();
this.renderChart();
this.renderTrendingItems();
} catch (error) {
console.error('Failed to initialize trending page:', error);
this.showError('Failed to load trending data');
}
}
async loadData() {
try {
const response = await fetch('trending-data.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.data = await response.json();
} catch (error) {
console.error('Error loading trending data:', error);
throw error;
}
}
setupEventListeners() {
// Component tabs
document.querySelectorAll('.component-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
// Remove active class from all tabs
document.querySelectorAll('.component-tab').forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
e.target.classList.add('active');
// Update current type
this.currentType = e.target.dataset.type;
this.renderTrendingItems();
});
});
// Date range filter
const dateRange = document.getElementById('date-range');
if (dateRange) {
dateRange.addEventListener('change', (e) => {
this.currentRange = e.target.value;
this.renderTrendingItems();
});
}
}
renderHeroStats() {
const container = document.getElementById('hero-stats');
if (!container || !this.data || !this.data.globalStats) {
return;
}
// Use real global statistics from the JSON
const globalStats = this.data.globalStats;
const stats = [
{
number: globalStats.totalComponents.toLocaleString(),
label: 'Unique Components',
change: 'Across 7 categories',
positive: true
},
{
number: globalStats.totalDownloads.toLocaleString(),
label: 'Total Downloads',
change: 'All time downloads',
positive: true
},
{
number: globalStats.totalCountries.toLocaleString(),
label: 'Countries',
change: 'Global reach',
positive: true
}
];
container.innerHTML = stats.map(stat => `
<div class="hero-stat-item">
<div class="hero-stat-number">${stat.number}</div>
<div class="hero-stat-label">${stat.label}</div>
<div class="hero-stat-change ${stat.positive ? '' : 'negative'}">
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
<path d="${stat.positive ? 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z' : 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM3.25 8a.75.75 0 0 1 .75-.75h8a.75.75 0 0 1 0 1.5h-8A.75.75 0 0 1 3.25 8z'}"/>
</svg>
${stat.change}
</div>
</div>
`).join('');
}
renderPopularItems() {
const container = document.getElementById('popular-categories');
if (!container || !this.data || !this.data.trending) {
return;
}
// Define categories to show and their display info (excluding templates)
const categories = [
{ key: 'agents', title: 'Agents', icon: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z' },
{ key: 'commands', title: 'Commands', icon: 'M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM2.5 8a5.5 5.5 0 1 0 11 0 5.5 5.5 0 0 0-11 0z' },
{ key: 'mcps', title: 'MCPs', icon: 'M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c0 .026.009.051.025.072L2.5 7a.5.5 0 0 1 0 .708L1.025 9.133a.149.149 0 0 0-.025.072V10.5A1.5 1.5 0 0 0 2.5 12h2.793a.149.149 0 0 0 .072-.025L7 10.5a.5.5 0 0 1 .708 0l1.625 1.475c.021.016.046.025.072.025H12.5A1.5 1.5 0 0 0 14 10.5v-.793a.149.149 0 0 0-.025-.072L12.5 8a.5.5 0 0 1 0-.708l1.475-1.625a.149.149 0 0 0 .025-.072V4.5A1.5 1.5 0 0 0 12.5 3H9.707a.149.149 0 0 0-.072.025L8 4.5a.5.5 0 0 1-.708 0L5.867 3.025A.149.149 0 0 0 5.793 3H2.5z' },
{ key: 'settings', title: 'Settings', icon: 'M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z' },
{ key: 'hooks', title: 'Hooks', icon: 'M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4.2-4.2a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z' },
{ key: 'skills', title: 'Skills', icon: 'M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v8A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5zm1.886 6.914L15 7.151V12.5a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5V7.15l6.614 1.764a1.5 1.5 0 0 0 .772 0zM1.5 4h13a.5.5 0 0 1 .5.5v1.616L8.129 7.948a.5.5 0 0 1-.258 0L1 6.116V4.5a.5.5 0 0 1 .5-.5z' }
];
container.innerHTML = '';
categories.forEach(category => {
const categoryData = this.data.trending[category.key];
if (!categoryData || !Array.isArray(categoryData) || categoryData.length === 0) {
return;
}
// Sort by monthly downloads and take only the top 1
const topItem = categoryData
.sort((a, b) => (b.downloadsMonth || 0) - (a.downloadsMonth || 0))[0];
const itemElement = this.createPopularItemCard(category, topItem);
container.appendChild(itemElement);
});
}
renderTopCountries() {
const container = document.getElementById('top-countries-list');
if (!container || !this.data || !this.data.topCountries) {
return;
}
const topCountries = this.data.topCountries;
container.innerHTML = topCountries.map(country => `
<div class="country-item">
<div class="country-flag">${country.flag}</div>
<div class="country-info">
<div class="country-name">${country.name}</div>
<div class="country-stats">
<span class="country-downloads">${country.downloads.toLocaleString()}</span>
<span class="country-percentage">(${country.percentage}%)</span>
</div>
<div class="country-bar-container">
<div class="country-bar" style="width: ${country.percentage}%"></div>
</div>
</div>
</div>
`).join('');
}
createPopularItemCard(category, item) {
const itemElement = document.createElement('div');
itemElement.className = 'popular-item';
const totalDownloads = item.downloadsTotal || 0;
itemElement.innerHTML = `
<div class="popular-item-header">
<div class="popular-item-info">
<div class="popular-item-type">${category.title}</div>
<h4 class="popular-item-name">${item.name}</h4>
</div>
</div>
<div class="popular-total-downloads">
<span class="total-number">${totalDownloads.toLocaleString()}</span>
<span class="total-label">downloads</span>
</div>
<div class="popular-stats">
<button class="popular-install-btn" onclick="showInstallModal('${item.id || item.name}')">
<svg class="popular-install-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Get
</button>
</div>
`;
return itemElement;
}
renderChart() {
if (!this.data || !this.data.chartData) {
console.warn('No chart data available');
return;
}
const ctx = document.getElementById('downloadsChart');
if (!ctx) {
console.warn('Chart canvas not found');
return;
}
// Chart colors matching terminal theme (excluding templates)
const colors = {
commands: '#10b981', // emerald (green)
agents: '#f59e0b', // amber (yellow)
mcps: '#3b82f6', // blue
settings: '#8b5cf6', // violet
hooks: '#f97316', // orange
skills: '#ec4899' // pink
};
// Define the desired order for the legend (agents first - most popular, excluding templates)
const categoryOrder = ['agents', 'commands', 'mcps', 'settings', 'hooks', 'skills'];
// Prepare datasets in the specified order
const datasets = categoryOrder
.filter(category => this.data.chartData.series[category]) // Only include categories that exist
.map(category => ({
label: category.charAt(0).toUpperCase() + category.slice(1),
data: this.data.chartData.series[category],
borderColor: colors[category] || '#6b7280',
backgroundColor: (colors[category] || '#6b7280') + '20',
borderWidth: 2,
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 4,
pointBackgroundColor: colors[category] || '#6b7280',
pointBorderColor: '#1f2937',
pointBorderWidth: 2
}));
// Create chart
new Chart(ctx, {
type: 'line',
data: {
labels: this.data.chartData.dates.map(date => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#d1d5db',
usePointStyle: true,
pointStyle: 'circle',
padding: 20,
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 11
}
}
},
tooltip: {
backgroundColor: '#1f2937',
titleColor: '#f3f4f6',
bodyColor: '#d1d5db',
borderColor: '#374151',
borderWidth: 1,
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
filter: function(tooltipItem, data) {
return true; // Show all items
},
itemSort: function(a, b) {
// Sort tooltip items by download count (highest to lowest)
return b.parsed.y - a.parsed.y;
},
callbacks: {
title: function(context) {
return `Date: ${context[0].label}`;
},
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toLocaleString()} downloads`;
},
labelColor: function(context) {
// Use the same colors as defined for the chart lines
const category = context.dataset.label.toLowerCase();
const color = colors[category] || '#6b7280';
return {
borderColor: color,
backgroundColor: color
};
}
}
}
},
scales: {
x: {
grid: {
color: '#374151',
borderColor: '#4b5563'
},
ticks: {
color: '#9ca3af',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 10
}
}
},
y: {
grid: {
color: '#374151',
borderColor: '#4b5563'
},
ticks: {
color: '#9ca3af',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 10
},
callback: function(value) {
return value.toLocaleString();
}
}
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
renderTrendingItems() {
const container = document.getElementById('trending-list');
const items = this.getFilteredItems();
if (items.length === 0) {
container.innerHTML = this.getEmptyState();
return;
}
container.innerHTML = '';
items.forEach((item, index) => {
const itemElement = this.createTrendingItem(item, index + 1);
container.appendChild(itemElement);
});
}
getFilteredItems() {
let allItems = [];
if (this.currentType === '') {
// If no filter, get items from "all" category if it exists,
// otherwise combine all categories
if (this.data.trending.all) {
allItems = this.data.trending.all;
} else {
Object.values(this.data.trending).forEach(categoryItems => {
if (Array.isArray(categoryItems)) {
allItems = allItems.concat(categoryItems);
}
});
}
} else {
// Get items from selected category
allItems = this.data.trending[this.currentType] || [];
}
// Sort by the current date range downloads (highest to lowest)
allItems.sort((a, b) => {
const aDownloads = this.getDownloadsForRange(a);
const bDownloads = this.getDownloadsForRange(b);
return bDownloads - aDownloads;
});
// For "all" categories, limit to top 10
if (this.currentType === '') {
allItems = allItems.slice(0, 10);
}
return allItems;
}
createTrendingItem(item, rank) {
const itemElement = document.createElement('div');
itemElement.className = 'trending-item';
const category = item.category || 'general';
const itemType = this.getItemType(item);
const componentType = this.getComponentTypeLabel(item);
const downloads = this.getDownloadsForRange(item);
itemElement.innerHTML = `
<div class="trending-rank">
<span class="rank-number">#${rank}</span>
</div>
<div class="trending-content">
<div class="trending-header">
<div class="trending-title">
<svg class="trending-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="${this.getIconPath(itemType)}"/>
</svg>
<h3 class="trending-name">${item.name}</h3>
<span class="trending-category">${category}</span>
</div>
<button class="install-button" onclick="showInstallModal('${item.id || item.name}')">
<svg class="install-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Install
</button>
</div>
<div class="trending-metadata">
<div class="trending-downloads">
<svg class="meta-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
<span>${downloads.toLocaleString()} downloads ${this.getRangeLabel()}</span>
</div>
<div class="trending-type-badge">${componentType}</div>
</div>
</div>
`;
return itemElement;
}
getDownloadsForRange(item) {
switch(this.currentRange) {
case 'today':
return item.downloadsToday || 0;
case 'week':
return item.downloadsWeek || 0;
case 'month':
return item.downloadsMonth || 0;
default:
return item.downloadsToday || 0;
}
}
getRangeLabel() {
switch(this.currentRange) {
case 'today':
return 'today';
case 'week':
return 'this week';
case 'month':
return 'this month';
default:
return 'today';
}
}
getComponentTypeLabel(item) {
// Determine the component type from the item ID or context
const id = item.id || '';
if (id.includes('agent-') || id.startsWith('agent') || this.currentType === 'agents') {
return 'Agent';
} else if (id.includes('command-') || id.startsWith('command') || this.currentType === 'commands') {
return 'Command';
} else if (id.includes('setting-') || id.startsWith('setting') || this.currentType === 'settings') {
return 'Settings';
} else if (id.includes('hook-') || id.startsWith('hook') || this.currentType === 'hooks') {
return 'Hooks';
} else if (id.includes('mcp-') || id.startsWith('mcp') || this.currentType === 'mcps') {
return 'MCP';
} else if (id.includes('skill-') || id.startsWith('skill') || this.currentType === 'skills') {
return 'Skill';
} else if (id.includes('template-') || id.startsWith('template') || this.currentType === 'templates') {
return 'Template';
}
// Fallback: try to determine from data structure
if (item.expertise) return 'Agent';
if (item.command) return 'Command';
return 'Component';
}
renderContributors(contributors) {
if (!contributors || contributors.length === 0) {
return '';
}
const contributorElements = contributors.slice(0, 3).map(contributor =>
`<img class="contributor-avatar" src="${contributor.avatar}" alt="${contributor.name}" title="${contributor.name}">`
).join('');
return `
<div class="meta-item">
<span>Built by</span>
<div class="contributors">
${contributorElements}
</div>
</div>
`;
}
renderTags(tags) {
if (!tags || tags.length === 0) {
return '';
}
const tagElements = tags.map(tag =>
`<span class="tag">${tag}</span>`
).join('');
return `<div class="tags">${tagElements}</div>`;
}
getLanguageClass(language) {
const languageMap = {
'JavaScript/TypeScript': 'typescript',
'JavaScript': 'javascript',
'TypeScript': 'typescript',
'Node.js/Express': 'nodejs',
'Python': 'python',
'SQL/Node.js': 'sql',
'Docker': 'docker',
'C#/Unity': 'csharp',
'Unity/Game Development': 'unity',
'Git/Bash': 'git',
'Multi-language': 'javascript',
'Universal': 'javascript'
};
return languageMap[language] || 'javascript';
}
getItemType(item) {
// Determine item type based on data structure or properties
if (item.expertise) return 'agents';
if (item.command) return 'commands';
if (item.type === 'settings') return 'settings';
if (item.type === 'hooks') return 'hooks';
if (item.type === 'mcps') return 'mcps';
if (item.type === 'skills') return 'skills';
if (item.type === 'templates') return 'templates';
return 'commands'; // default
}
getIconPath(type) {
const icons = {
commands: 'M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM2.5 8a5.5 5.5 0 1 0 11 0 5.5 5.5 0 0 0-11 0z',
agents: 'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z',
settings: 'M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z',
hooks: 'M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4.2-4.2a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z',
mcps: 'M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c0 .026.009.051.025.072L2.5 7a.5.5 0 0 1 0 .708L1.025 9.133a.149.149 0 0 0-.025.072V10.5A1.5 1.5 0 0 0 2.5 12h2.793a.149.149 0 0 0 .072-.025L7 10.5a.5.5 0 0 1 .708 0l1.625 1.475c.021.016.046.025.072.025H12.5A1.5 1.5 0 0 0 14 10.5v-.793a.149.149 0 0 0-.025-.072L12.5 8a.5.5 0 0 1 0-.708l1.475-1.625a.149.149 0 0 0 .025-.072V4.5A1.5 1.5 0 0 0 12.5 3H9.707a.149.149 0 0 0-.072.025L8 4.5a.5.5 0 0 1-.708 0L5.867 3.025A.149.149 0 0 0 5.793 3H2.5z',
skills: 'M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v8A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5zm1.886 6.914L15 7.151V12.5a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5V7.15l6.614 1.764a1.5 1.5 0 0 0 .772 0zM1.5 4h13a.5.5 0 0 1 .5.5v1.616L8.129 7.948a.5.5 0 0 1-.258 0L1 6.116V4.5a.5.5 0 0 1 .5-.5z',
templates: 'M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H1.75zM7.25 4a.75.75 0 0 1 1.5 0v4.25H12a.75.75 0 0 1 0 1.5H8.75V12a.75.75 0 0 1-1.5 0V9.75H4a.75.75 0 0 1 0-1.5h3.25V4z'
};
return icons[type] || icons.commands;
}
getEmptyState() {
return `
<div class="empty-state">
<svg class="empty-state-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773l4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"/>
</svg>
<h3>No trending items found</h3>
<p>Try adjusting your filters or check back later for new trending content.</p>
</div>
`;
}
showError(message) {
const container = document.getElementById('trending-list');
container.innerHTML = `
<div class="empty-state">
<svg class="empty-state-icon" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<h3>Error Loading Data</h3>
<p>${message}</p>
</div>
`;
}
showLoading() {
const container = document.getElementById('trending-list');
container.innerHTML = `
<div class="loading">
<div class="loading-spinner"></div>
<p>Loading trending components...</p>
</div>
`;
}
}
// Initialize the trending page when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.trendingPageInstance = new TrendingPage();
});
// Modal functionality
function showInstallModal(componentName) {
const modal = document.getElementById('installModal');
const commandText = document.getElementById('commandText');
// Determine the component type from the current filter or component name
let componentType = 'command'; // default
let componentCategory = '';
// Get the current trending page instance to check the type
if (window.trendingPageInstance) {
componentType = window.trendingPageInstance.currentType || 'commands';
// Find the component in the data to get its category
const items = window.trendingPageInstance.data.trending[componentType] || [];
const item = items.find(i => (i.id || i.name) === componentName);
if (item && item.category) {
componentCategory = item.category;
}
}
// Convert plural to singular for the command flag
const typeMap = {
'agents': 'agent',
'commands': 'command',
'settings': 'setting',
'hooks': 'hook',
'mcps': 'mcp',
'skills': 'skill',
'templates': 'template'
};
const flagType = typeMap[componentType] || 'command';
// Clean the component name by removing prefixes
let cleanName = componentName;
const prefixesToRemove = ['agent-', 'command-', 'setting-', 'hook-', 'mcp-', 'skill-', 'template-'];
for (const prefix of prefixesToRemove) {
if (cleanName.startsWith(prefix)) {
cleanName = cleanName.substring(prefix.length);
break;
}
}
// Build the full component path: category/name
const componentPath = componentCategory ? `${componentCategory}/${cleanName}` : cleanName;
// Update the command with the correct flag and full component path
const command = `npx claude-code-templates@latest --${flagType} ${componentPath} --yes`;
commandText.textContent = command;
// Show the modal
modal.classList.add('show');
// Prevent body scrolling
document.body.style.overflow = 'hidden';
}
function closeInstallModal() {
const modal = document.getElementById('installModal');
modal.classList.remove('show');
// Restore body scrolling
document.body.style.overflow = '';
// Hide copy feedback
const feedback = document.getElementById('copyFeedback');
feedback.classList.remove('show');
}
function copyInstallCommand() {
const commandText = document.getElementById('commandText');
const copyFeedback = document.getElementById('copyFeedback');
const copyButton = document.querySelector('.copy-button');
// Copy to clipboard
navigator.clipboard.writeText(commandText.textContent).then(() => {
// Show success feedback
copyFeedback.classList.add('show');
// Temporarily change button text
const originalText = copyButton.innerHTML;
copyButton.innerHTML = `
<svg class="copy-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
Copied!
`;
// Reset after 2 seconds
setTimeout(() => {
copyButton.innerHTML = originalText;
copyFeedback.classList.remove('show');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
// Fallback for older browsers
fallbackCopyTextToClipboard(commandText.textContent);
});
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.width = "2em";
textArea.style.height = "2em";
textArea.style.padding = "0";
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
textArea.style.background = "transparent";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
const copyFeedback = document.getElementById('copyFeedback');
copyFeedback.classList.add('show');
setTimeout(() => {
copyFeedback.classList.remove('show');
}, 2000);
} catch (err) {
console.error('Fallback: Could not copy text: ', err);
}
document.body.removeChild(textArea);
}
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeInstallModal();
}
});
// Add some utility functions for future enhancements
window.TrendingUtils = {
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
},
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
};