mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-09-19 01:30:23 +08:00
cf7e008c59
- Create 3 Neon tables: installation_outcomes, website_events, api_health_logs - Add API endpoints: track-installation-outcome, track-website-events, health-check - Track CLI installation success/failure with timing and error classification - Add website event tracker (search, cart, component views) with batched sendBeacon - Add health-check cron every 15min with Discord alerts on failure - Integrate tracking in all 6 installIndividual* functions with batchId support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1916 lines
81 KiB
JavaScript
1916 lines
81 KiB
JavaScript
// Component Page JavaScript
|
||
class ComponentPageManager {
|
||
constructor() {
|
||
this.component = null;
|
||
this.dataLoader = null;
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
// Initialize data loader
|
||
this.dataLoader = window.dataLoader || new DataLoader();
|
||
|
||
// Load component from URL parameters
|
||
await this.loadComponentFromURL();
|
||
|
||
// Setup event listeners
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
async loadComponentFromURL() {
|
||
let componentType, componentName, componentPath;
|
||
|
||
// Check if we're using SEO-friendly URLs: /component/type/name
|
||
const pathParts = window.location.pathname.split('/').filter(part => part);
|
||
if (pathParts.length >= 3 && pathParts[0] === 'component') {
|
||
componentType = decodeURIComponent(pathParts[1]);
|
||
componentName = decodeURIComponent(pathParts[2]);
|
||
} else {
|
||
// Fallback to query parameters
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
componentType = urlParams.get('type');
|
||
componentName = urlParams.get('name');
|
||
componentPath = urlParams.get('path');
|
||
}
|
||
|
||
if (!componentType || (!componentName && !componentPath)) {
|
||
this.showError('Missing component parameters in URL');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Load all components data
|
||
const componentsData = await this.dataLoader.loadAllComponents();
|
||
|
||
if (!componentsData) {
|
||
throw new Error('Failed to load components data');
|
||
}
|
||
|
||
// Find the component
|
||
const categoryKey = componentType + 's'; // Convert 'agent' to 'agents'
|
||
const components = componentsData[categoryKey] || [];
|
||
|
||
let component = null;
|
||
|
||
// Try to find by name first, then by path
|
||
if (componentName) {
|
||
component = components.find(c => c.name === componentName);
|
||
|
||
// If not found, try to find by path that ends with the component name
|
||
if (!component) {
|
||
component = components.find(c => {
|
||
// Check if path ends with componentName.md or componentName.json
|
||
const pathEnd = `${componentName}.md`;
|
||
const pathEndJson = `${componentName}.json`;
|
||
return c.path && (c.path.endsWith(pathEnd) || c.path.endsWith(pathEndJson));
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!component && componentPath) {
|
||
component = components.find(c => c.path === componentPath);
|
||
}
|
||
|
||
if (!component) {
|
||
throw new Error(`Component not found: ${componentType}/${componentName || componentPath}`);
|
||
}
|
||
|
||
this.component = {
|
||
...component,
|
||
type: componentType
|
||
};
|
||
|
||
await this.renderComponent();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading component:', error);
|
||
this.showError(error.message);
|
||
}
|
||
}
|
||
|
||
async renderComponent() {
|
||
if (!this.component) {
|
||
this.showError('No component data available');
|
||
return;
|
||
}
|
||
|
||
// Hide loading state
|
||
document.getElementById('loadingState').style.display = 'none';
|
||
|
||
// Show component content
|
||
document.getElementById('componentContent').style.display = 'block';
|
||
|
||
// Render component header
|
||
this.renderComponentHeader();
|
||
|
||
// Render component description
|
||
this.renderComponentDescription();
|
||
|
||
// Render security validation section
|
||
this.renderSecurityValidation();
|
||
|
||
// Render metadata section
|
||
await this.renderMetadataSection();
|
||
|
||
// Render installation section
|
||
this.renderInstallationSection();
|
||
|
||
// Render component code
|
||
await this.renderComponentCode();
|
||
|
||
// Render GitHub link
|
||
this.renderGitHubLink();
|
||
|
||
// Populate sidebar quick install
|
||
this.renderSidebarQuickInstall();
|
||
|
||
// Update page metadata
|
||
this.updatePageMetadata();
|
||
|
||
// Load Giscus comments
|
||
this.loadGiscusComments();
|
||
|
||
// Activate tab from URL hash
|
||
this.activateTabFromHash();
|
||
|
||
// Track component view
|
||
window.eventTracker?.track('component_view', {
|
||
component_type: this.component.type,
|
||
component_name: this.component.name,
|
||
category: this.component.category || null
|
||
});
|
||
}
|
||
|
||
|
||
renderComponentHeader() {
|
||
const typeConfig = {
|
||
agent: { icon: '🤖', color: '#ff6b6b', badge: 'AGENT' },
|
||
command: { icon: '⚡', color: '#4ecdc4', badge: 'COMMAND' },
|
||
mcp: { icon: '🔌', color: '#45b7d1', badge: 'MCP' },
|
||
setting: { icon: '⚙️', color: '#9c88ff', badge: 'SETTING' },
|
||
hook: { icon: '🪝', color: '#ff8c42', badge: 'HOOK' },
|
||
template: { icon: '📦', color: '#f9a825', badge: 'TEMPLATE' }
|
||
};
|
||
|
||
const config = typeConfig[this.component.type] || typeConfig.template;
|
||
const formattedName = this.formatComponentName(this.component.name);
|
||
|
||
// Update icon with null check
|
||
const iconElement = document.getElementById('componentIcon');
|
||
if (iconElement) {
|
||
iconElement.textContent = config.icon;
|
||
}
|
||
|
||
// Update title with null check
|
||
const titleElement = document.getElementById('componentTitle');
|
||
if (titleElement) {
|
||
titleElement.textContent = formattedName;
|
||
}
|
||
|
||
// Update type badge with null check
|
||
const typeBadge = document.getElementById('componentTypeBadge');
|
||
if (typeBadge) {
|
||
typeBadge.textContent = config.badge;
|
||
typeBadge.style.backgroundColor = config.color;
|
||
}
|
||
|
||
// Update category - restored for original design
|
||
const category = this.component.category || 'General';
|
||
const categoryElement = document.getElementById('componentCategory');
|
||
if (categoryElement) {
|
||
categoryElement.textContent = category.charAt(0).toUpperCase() + category.slice(1);
|
||
}
|
||
|
||
// Update download badge
|
||
const downloadBadge = document.getElementById('componentDownloadBadge');
|
||
const downloadCount = document.getElementById('downloadCount');
|
||
if (downloadBadge && downloadCount) {
|
||
const downloads = this.component.downloads || 0;
|
||
if (downloads > 0) {
|
||
downloadBadge.style.display = 'inline-flex';
|
||
downloadCount.textContent = this.formatNumber(downloads);
|
||
downloadBadge.title = `${downloads.toLocaleString()} downloads`;
|
||
} else {
|
||
downloadBadge.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Add to cart button is set up in setupEventListeners()
|
||
}
|
||
|
||
renderComponentDescription() {
|
||
const description = this.getComponentDescription();
|
||
const descriptionElement = document.getElementById('componentDescription');
|
||
if (descriptionElement) {
|
||
descriptionElement.textContent = description;
|
||
}
|
||
}
|
||
|
||
renderSecurityValidation() {
|
||
const security = this.component.security;
|
||
|
||
// If no security data, hide everything
|
||
if (!security || !security.validated) {
|
||
const validationSection = document.getElementById('validationSection');
|
||
if (validationSection) validationSection.style.display = 'none';
|
||
|
||
const validationBadge = document.getElementById('componentValidationBadge');
|
||
if (validationBadge) validationBadge.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Show validation section
|
||
const validationSection = document.getElementById('validationSection');
|
||
if (validationSection) validationSection.style.display = 'block';
|
||
|
||
const score = security.score || 0;
|
||
const isValid = security.valid;
|
||
const errors = security.errorCount || 0;
|
||
const warnings = security.warningCount || 0;
|
||
|
||
// Determine color based on score
|
||
let scoreColor = '#48bb78'; // Green
|
||
let scoreStatus = 'Excellent';
|
||
|
||
if (score < 70) {
|
||
scoreColor = '#f56565'; // Red
|
||
scoreStatus = 'Needs Attention';
|
||
} else if (score < 85) {
|
||
scoreColor = '#ed8936'; // Orange
|
||
scoreStatus = 'Good';
|
||
} else if (score < 100) {
|
||
scoreColor = '#48bb78'; // Green
|
||
scoreStatus = 'Very Good';
|
||
} else if (score === 100) {
|
||
scoreColor = '#48bb78'; // Green
|
||
scoreStatus = 'Excellent';
|
||
}
|
||
|
||
// Only override to red if score is below 70 OR (invalid AND low errors)
|
||
if (!isValid && score < 70) {
|
||
scoreColor = '#f56565'; // Red
|
||
scoreStatus = 'Needs Attention';
|
||
}
|
||
|
||
// Update header badge
|
||
const validationBadge = document.getElementById('componentValidationBadge');
|
||
const validationScoreSpan = document.getElementById('validationScore');
|
||
if (validationBadge && validationScoreSpan) {
|
||
validationBadge.style.display = 'inline-flex';
|
||
validationBadge.style.backgroundColor = scoreColor + '20';
|
||
validationBadge.style.borderColor = scoreColor + '50';
|
||
validationBadge.style.color = scoreColor;
|
||
validationScoreSpan.textContent = score;
|
||
validationBadge.title = `Quality Score: ${score}/100 - Click to see details`;
|
||
validationBadge.style.cursor = 'pointer';
|
||
|
||
// Add click event to scroll to validation section
|
||
validationBadge.onclick = function() {
|
||
const validationSection = document.getElementById('validationSection');
|
||
if (validationSection) {
|
||
validationSection.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'start'
|
||
});
|
||
|
||
// Add a highlight animation
|
||
validationSection.style.animation = 'highlight 1s ease';
|
||
setTimeout(() => {
|
||
validationSection.style.animation = '';
|
||
}, 1000);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Update score circle
|
||
const scoreCircle = document.getElementById('scoreCircle');
|
||
const scoreValue = document.getElementById('scoreValue');
|
||
if (scoreCircle && scoreValue) {
|
||
scoreCircle.style.borderColor = scoreColor;
|
||
scoreValue.textContent = score;
|
||
scoreValue.style.color = scoreColor;
|
||
}
|
||
|
||
// Update score status
|
||
const scoreStatusEl = document.getElementById('scoreStatus');
|
||
if (scoreStatusEl) {
|
||
scoreStatusEl.textContent = scoreStatus;
|
||
scoreStatusEl.style.color = scoreColor;
|
||
}
|
||
|
||
// Update inline stats
|
||
const inlineErrors = document.getElementById('inlineErrors');
|
||
const inlineWarnings = document.getElementById('inlineWarnings');
|
||
if (inlineErrors) {
|
||
inlineErrors.textContent = errors;
|
||
inlineErrors.style.color = errors > 0 ? '#f56565' : '#48bb78';
|
||
}
|
||
if (inlineWarnings) {
|
||
inlineWarnings.textContent = warnings;
|
||
inlineWarnings.style.color = warnings > 0 ? '#ed8936' : '#48bb78';
|
||
}
|
||
|
||
// Render validation checks with detailed messages
|
||
const validationChecks = document.getElementById('validationChecks');
|
||
if (validationChecks && security.validators) {
|
||
const checkDescriptions = {
|
||
structural: 'Verifies file format, YAML frontmatter, required fields, and encoding',
|
||
integrity: 'Checks for tampering using SHA256 hash and version tracking',
|
||
semantic: 'Detects malicious patterns, prompt injection, and dangerous commands',
|
||
references: 'Validates external URLs and prevents SSRF attacks',
|
||
provenance: 'Confirms author metadata and repository information'
|
||
};
|
||
|
||
const checkLabels = {
|
||
structural: 'Structure',
|
||
integrity: 'Integrity',
|
||
semantic: 'Content Safety',
|
||
references: 'references',
|
||
provenance: 'Source'
|
||
};
|
||
|
||
validationChecks.innerHTML = '';
|
||
Object.entries(security.validators).forEach(([key, validatorData]) => {
|
||
// Handle both old format (boolean) and new format (object with detailed data)
|
||
const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid;
|
||
const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0;
|
||
const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0;
|
||
const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0;
|
||
const hasDetails = hasErrors || hasWarnings || hasInfo || passed;
|
||
|
||
// Determine status: passed, warning, or failed
|
||
let status = 'passed';
|
||
let icon = '✓';
|
||
if (hasErrors) {
|
||
status = 'failed';
|
||
icon = '✗';
|
||
} else if (hasWarnings) {
|
||
status = 'warning';
|
||
icon = '⚠';
|
||
}
|
||
|
||
const checkItem = document.createElement('div');
|
||
checkItem.className = `validation-check-item ${status}`;
|
||
checkItem.style.cursor = 'pointer';
|
||
|
||
// Build HTML for check item
|
||
checkItem.innerHTML = `
|
||
<div class="check-header">
|
||
<div class="check-main">
|
||
<span class="check-icon">${icon}</span>
|
||
<span class="check-label">${checkLabels[key] || key}</span>
|
||
</div>
|
||
<div class="check-stats">
|
||
${typeof validatorData === 'object' ? `
|
||
${validatorData.errorCount > 0 ? `<span class="check-error-count">${validatorData.errorCount} errors</span>` : ''}
|
||
${validatorData.warningCount > 0 ? `<span class="check-warning-count">${validatorData.warningCount} warnings</span>` : ''}
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add click event listener - all items are clickable now
|
||
checkItem.addEventListener('click', () => {
|
||
window.openValidationModal(key, checkLabels[key] || key, validatorData, checkDescriptions[key]);
|
||
});
|
||
|
||
validationChecks.appendChild(checkItem);
|
||
});
|
||
}
|
||
}
|
||
|
||
async renderMetadataSection() {
|
||
const metadataSection = document.getElementById('metadataSection');
|
||
|
||
// Check if component has metadata fields directly from frontmatter
|
||
const hasDirectAuthor = this.component.author && this.component.author.trim() !== '';
|
||
const hasDirectRepo = this.component.repo && this.component.repo.trim() !== '';
|
||
const hasDirectVersion = this.component.version && this.component.version.trim() !== '';
|
||
const hasDirectLicense = this.component.license && this.component.license.trim() !== '';
|
||
const hasDirectKeywords = this.component.keywords && this.component.keywords.length > 0;
|
||
|
||
// For agents, try to load marketplace data
|
||
let agentMetadata = null;
|
||
if (this.component.type === 'agent') {
|
||
try {
|
||
const marketplace = await this.loadComponentsMarketplace();
|
||
|
||
if (marketplace && marketplace.agents) {
|
||
const componentName = this.component.name.replace('.md', '');
|
||
|
||
agentMetadata = marketplace.agents.find(
|
||
agent => agent.name === componentName
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading marketplace metadata:', error);
|
||
}
|
||
}
|
||
|
||
// Show metadata section if we have any metadata
|
||
const hasMetadata = hasDirectAuthor || hasDirectRepo || hasDirectVersion || hasDirectLicense || hasDirectKeywords || agentMetadata;
|
||
|
||
if (!hasMetadata) {
|
||
if (metadataSection) metadataSection.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// Show metadata section
|
||
if (metadataSection) metadataSection.style.display = 'block';
|
||
|
||
// Populate version - prioritize direct version field, then marketplace
|
||
const versionElement = document.getElementById('metadataVersion');
|
||
if (versionElement) {
|
||
const version = this.component.version || agentMetadata?.version || '--';
|
||
versionElement.textContent = version;
|
||
}
|
||
|
||
// Populate author - prioritize direct author field, then marketplace
|
||
const authorElement = document.getElementById('metadataAuthor');
|
||
if (authorElement) {
|
||
if (hasDirectAuthor) {
|
||
authorElement.textContent = this.component.author;
|
||
} else if (agentMetadata) {
|
||
const authorName = agentMetadata.author?.name || agentMetadata.author || '--';
|
||
authorElement.textContent = authorName;
|
||
} else {
|
||
authorElement.textContent = '--';
|
||
}
|
||
}
|
||
|
||
// Populate license - prioritize direct license field, then marketplace
|
||
const licenseElement = document.getElementById('metadataLicense');
|
||
if (licenseElement) {
|
||
const license = this.component.license || agentMetadata?.license || '--';
|
||
licenseElement.textContent = license;
|
||
}
|
||
|
||
// Populate repository - prioritize direct repo field, then marketplace
|
||
const repositoryLink = document.getElementById('metadataRepository');
|
||
const repositoryNone = document.getElementById('metadataRepositoryNone');
|
||
|
||
let repositoryUrl = null;
|
||
|
||
// Priority 1: Direct repo field
|
||
if (hasDirectRepo) {
|
||
repositoryUrl = this.component.repo;
|
||
}
|
||
// Priority 2: Marketplace repository
|
||
else if (agentMetadata?.repository) {
|
||
repositoryUrl = agentMetadata.repository;
|
||
}
|
||
|
||
if (repositoryUrl) {
|
||
if (repositoryLink) {
|
||
repositoryLink.href = repositoryUrl;
|
||
repositoryLink.style.display = 'inline-flex';
|
||
}
|
||
if (repositoryNone) {
|
||
repositoryNone.style.display = 'none';
|
||
}
|
||
} else {
|
||
if (repositoryLink) {
|
||
repositoryLink.style.display = 'none';
|
||
}
|
||
if (repositoryNone) {
|
||
repositoryNone.style.display = 'inline';
|
||
}
|
||
}
|
||
|
||
// Populate keywords - prioritize direct keywords field, then marketplace
|
||
const keywordsContainer = document.getElementById('metadataKeywords');
|
||
if (keywordsContainer) {
|
||
const keywords = this.component.keywords || agentMetadata?.keywords || [];
|
||
if (keywords && keywords.length > 0) {
|
||
keywordsContainer.innerHTML = '';
|
||
keywords.forEach(keyword => {
|
||
const keywordChip = document.createElement('span');
|
||
keywordChip.className = 'metadata-keyword';
|
||
keywordChip.textContent = keyword;
|
||
keywordsContainer.appendChild(keywordChip);
|
||
});
|
||
} else {
|
||
keywordsContainer.innerHTML = '<span class="metadata-value">--</span>';
|
||
}
|
||
}
|
||
}
|
||
|
||
async loadComponentsMarketplace() {
|
||
try {
|
||
// Check if components marketplace is already cached
|
||
if (this.componentsMarketplace) {
|
||
return this.componentsMarketplace;
|
||
}
|
||
|
||
// Use the already loaded components data from dataLoader
|
||
const componentsData = await this.dataLoader.loadAllComponents();
|
||
|
||
if (componentsData && componentsData.componentsMarketplace) {
|
||
this.componentsMarketplace = componentsData.componentsMarketplace;
|
||
return this.componentsMarketplace;
|
||
}
|
||
|
||
// Fallback: try to fetch marketplace.json directly from components/.claude-plugin/
|
||
const marketplaceResponse = await fetch('https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/.claude-plugin/marketplace.json');
|
||
if (marketplaceResponse.ok) {
|
||
this.componentsMarketplace = await marketplaceResponse.json();
|
||
return this.componentsMarketplace;
|
||
}
|
||
|
||
console.warn('No components marketplace found');
|
||
return null;
|
||
} catch (error) {
|
||
console.error('Error loading components marketplace:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
renderInstallationSection() {
|
||
const componentPath = this.getCleanPath();
|
||
const basicInstallCommand = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`;
|
||
|
||
// Update basic installation command
|
||
const basicInstallElement = document.getElementById('basicInstallCommand');
|
||
if (basicInstallElement) {
|
||
basicInstallElement.textContent = basicInstallCommand;
|
||
}
|
||
|
||
// Handle agent-specific sections
|
||
if (this.component.type === 'agent') {
|
||
this.renderGlobalAgentSection(componentPath);
|
||
this.renderCloudSandboxSection(componentPath);
|
||
} else {
|
||
// Hide agent-specific sections for non-agents
|
||
const globalAgentSection = document.getElementById('globalAgentSection');
|
||
const cloudSandboxSection = document.getElementById('cloudSandboxSection');
|
||
|
||
if (globalAgentSection) globalAgentSection.style.display = 'none';
|
||
if (cloudSandboxSection) cloudSandboxSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
renderGlobalAgentSection(componentPath) {
|
||
const globalAgentSection = document.getElementById('globalAgentSection');
|
||
const globalAgentCommand = `npx claude-code-templates@latest --create-agent ${componentPath}`;
|
||
const globalUsageCommand = `${componentPath.split('/').pop()} "your prompt here"`;
|
||
|
||
const globalAgentCommandElement = document.getElementById('globalAgentCommand');
|
||
const globalUsageCommandElement = document.getElementById('globalUsageCommand');
|
||
|
||
if (globalAgentCommandElement) {
|
||
globalAgentCommandElement.textContent = globalAgentCommand;
|
||
}
|
||
if (globalUsageCommandElement) {
|
||
globalUsageCommandElement.textContent = globalUsageCommand;
|
||
}
|
||
|
||
if (globalAgentSection) {
|
||
globalAgentSection.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
renderCloudSandboxSection(componentPath) {
|
||
const cloudSandboxSection = document.getElementById('cloudSandboxSection');
|
||
|
||
// Update all sandbox command examples with the component path
|
||
const e2bSandboxCommand = `npx claude-code-templates@latest --sandbox e2b --agent=${componentPath} --prompt "your development task"`;
|
||
const cloudflareSandboxCommand = `npx claude-code-templates@latest --sandbox cloudflare --agent=${componentPath} --prompt "your development task"`;
|
||
const dockerSandboxCommand = `npx claude-code-templates@latest --sandbox docker --agent=${componentPath} --prompt "your development task"`;
|
||
|
||
const e2bSandboxCommandElement = document.getElementById('e2bSandboxCommand');
|
||
const cloudflareSandboxCommandElement = document.getElementById('cloudflareSandboxCommand');
|
||
const dockerSandboxCommandElement = document.getElementById('dockerSandboxCommand');
|
||
|
||
if (e2bSandboxCommandElement) {
|
||
e2bSandboxCommandElement.textContent = e2bSandboxCommand;
|
||
}
|
||
if (cloudflareSandboxCommandElement) {
|
||
cloudflareSandboxCommandElement.textContent = cloudflareSandboxCommand;
|
||
}
|
||
if (dockerSandboxCommandElement) {
|
||
dockerSandboxCommandElement.textContent = dockerSandboxCommand;
|
||
}
|
||
|
||
if (cloudSandboxSection) {
|
||
cloudSandboxSection.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async renderComponentCode() {
|
||
try {
|
||
let content = this.component.content || 'No content available.';
|
||
let language = 'plaintext';
|
||
|
||
// Determine language based on file extension
|
||
if (this.component.path) {
|
||
const extension = this.component.path.split('.').pop();
|
||
switch (extension) {
|
||
case 'md':
|
||
language = 'markdown';
|
||
break;
|
||
case 'json':
|
||
language = 'json';
|
||
// Pretty print JSON
|
||
try {
|
||
content = JSON.stringify(JSON.parse(content), null, 2);
|
||
} catch (e) {
|
||
console.warn('Could not parse JSON content:', e);
|
||
}
|
||
break;
|
||
case 'js':
|
||
language = 'javascript';
|
||
break;
|
||
case 'yml':
|
||
case 'yaml':
|
||
language = 'yaml';
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Collect line numbers with errors and warnings
|
||
const errorLines = new Set();
|
||
const warningLines = new Set();
|
||
|
||
if (this.component.security && this.component.security.validators) {
|
||
Object.values(this.component.security.validators).forEach(validator => {
|
||
if (validator.errors) {
|
||
validator.errors.forEach(error => {
|
||
// Check direct line property
|
||
if (error.line) {
|
||
errorLines.add(error.line);
|
||
}
|
||
// Check examples array
|
||
if (error.examples && error.examples.length > 0) {
|
||
error.examples.forEach(example => {
|
||
if (example.line) {
|
||
errorLines.add(example.line);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
if (validator.warnings) {
|
||
validator.warnings.forEach(warning => {
|
||
// Check direct line property
|
||
if (warning.line) {
|
||
warningLines.add(warning.line);
|
||
}
|
||
// Check examples array
|
||
if (warning.examples && warning.examples.length > 0) {
|
||
warning.examples.forEach(example => {
|
||
if (example.line) {
|
||
warningLines.add(example.line);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update code language indicator
|
||
const codeLanguageElement = document.getElementById('codeLanguage');
|
||
if (codeLanguageElement) {
|
||
codeLanguageElement.textContent = language;
|
||
}
|
||
|
||
// Render code with syntax highlighting
|
||
const codeElement = document.querySelector('#codeContent code');
|
||
if (codeElement) {
|
||
codeElement.innerHTML = this.highlightCode(content, language);
|
||
codeElement.className = `language-${language}`;
|
||
}
|
||
|
||
// Generate line numbers with error/warning indicators
|
||
const lines = content.split('\n');
|
||
const lineNumbers = lines.map((_, index) => {
|
||
const lineNum = index + 1;
|
||
let className = '';
|
||
let style = '';
|
||
let title = '';
|
||
|
||
if (errorLines.has(lineNum)) {
|
||
className = 'line-with-error';
|
||
style = 'background-color: rgba(245, 101, 101, 0.15); color: #f56565; font-weight: 600; border-left: 3px solid #f56565; padding-left: 5px; cursor: pointer;';
|
||
title = 'Line has validation errors - Click to see details';
|
||
} else if (warningLines.has(lineNum)) {
|
||
className = 'line-with-warning';
|
||
style = 'background-color: rgba(237, 137, 54, 0.15); color: #ed8936; font-weight: 600; border-left: 3px solid #ed8936; padding-left: 5px; cursor: pointer;';
|
||
title = 'Line has validation warnings - Click to see details';
|
||
}
|
||
|
||
return `<span class="${className}" style="${style}" title="${title}" data-line-number="${lineNum}">${lineNum}</span>`;
|
||
}).join('');
|
||
|
||
const lineNumbersElement = document.getElementById('lineNumbers');
|
||
if (lineNumbersElement) {
|
||
lineNumbersElement.innerHTML = lineNumbers;
|
||
|
||
// Add click event listeners to error/warning line numbers
|
||
const clickableLines = lineNumbersElement.querySelectorAll('.line-with-error, .line-with-warning');
|
||
clickableLines.forEach(lineSpan => {
|
||
lineSpan.addEventListener('click', () => {
|
||
// Find which validator has the error for this line
|
||
const lineNum = parseInt(lineSpan.getAttribute('data-line-number'));
|
||
|
||
if (this.component.security && this.component.security.validators) {
|
||
// Look for the validator with this line number
|
||
for (const [validatorKey, validatorData] of Object.entries(this.component.security.validators)) {
|
||
if (typeof validatorData === 'object') {
|
||
// Check if this validator has errors/warnings for this line
|
||
const hasError = validatorData.errors && validatorData.errors.some(error => {
|
||
if (error.line === lineNum) return true;
|
||
if (error.examples && error.examples.some(ex => ex.line === lineNum)) return true;
|
||
return false;
|
||
});
|
||
|
||
const hasWarning = validatorData.warnings && validatorData.warnings.some(warning => {
|
||
if (warning.line === lineNum) return true;
|
||
if (warning.examples && warning.examples.some(ex => ex.line === lineNum)) return true;
|
||
return false;
|
||
});
|
||
|
||
if (hasError || hasWarning) {
|
||
// Found the validator with this line, open its modal
|
||
const checkLabels = {
|
||
structural: 'Structure',
|
||
integrity: 'Integrity',
|
||
semantic: 'Content Safety',
|
||
references: 'references',
|
||
provenance: 'Source'
|
||
};
|
||
|
||
const checkDescriptions = {
|
||
structural: 'Verifies file format, YAML frontmatter, required fields, and encoding',
|
||
integrity: 'Checks for tampering using SHA256 hash and version tracking',
|
||
semantic: 'Detects malicious patterns, prompt injection, and dangerous commands',
|
||
references: 'Validates external URLs and prevents SSRF attacks',
|
||
provenance: 'Confirms author metadata and repository information'
|
||
};
|
||
|
||
window.openValidationModal(
|
||
validatorKey,
|
||
checkLabels[validatorKey] || validatorKey,
|
||
validatorData,
|
||
checkDescriptions[validatorKey]
|
||
);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Add corresponding highlighting to code lines
|
||
if (errorLines.size > 0 || warningLines.size > 0) {
|
||
const codeLines = codeElement.innerHTML.split('\n');
|
||
const highlightedLines = codeLines.map((line, index) => {
|
||
const lineNum = index + 1;
|
||
if (errorLines.has(lineNum)) {
|
||
return `<span style="background-color: rgba(245, 101, 101, 0.08); display: block; margin: 0 -10px; padding: 0 10px;">${line}</span>`;
|
||
} else if (warningLines.has(lineNum)) {
|
||
return `<span style="background-color: rgba(237, 137, 54, 0.08); display: block; margin: 0 -10px; padding: 0 10px;">${line}</span>`;
|
||
}
|
||
return line;
|
||
});
|
||
codeElement.innerHTML = highlightedLines.join('\n');
|
||
}
|
||
|
||
// Sync scroll between line numbers and code content
|
||
const codeContentDiv = document.getElementById('codeContent');
|
||
if (lineNumbersElement && codeContentDiv) {
|
||
codeContentDiv.addEventListener('scroll', function() {
|
||
lineNumbersElement.scrollTop = codeContentDiv.scrollTop;
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error rendering component code:', error);
|
||
const codeElement = document.querySelector('#codeContent code');
|
||
if (codeElement) {
|
||
codeElement.textContent = 'Error loading component content.';
|
||
}
|
||
}
|
||
}
|
||
|
||
loadGiscusComments() {
|
||
const container = document.getElementById('giscusContainer');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
const script = document.createElement('script');
|
||
script.src = 'https://giscus.app/client.js';
|
||
script.setAttribute('data-repo', 'davila7/claude-code-templates');
|
||
script.setAttribute('data-repo-id', 'R_kgDOPGh7XA');
|
||
script.setAttribute('data-category', 'Comments');
|
||
script.setAttribute('data-category-id', 'DIC_kwDOPGh7XM4C2H0y');
|
||
script.setAttribute('data-mapping', 'pathname');
|
||
script.setAttribute('data-strict', '0');
|
||
script.setAttribute('data-reactions-enabled', '0');
|
||
script.setAttribute('data-emit-metadata', '0');
|
||
script.setAttribute('data-input-position', 'bottom');
|
||
script.setAttribute('data-theme', 'dark');
|
||
script.setAttribute('data-lang', 'en');
|
||
script.setAttribute('crossorigin', 'anonymous');
|
||
script.async = true;
|
||
|
||
container.appendChild(script);
|
||
}
|
||
|
||
renderSidebarQuickInstall() {
|
||
const commandEl = document.getElementById('quickInstallCommand');
|
||
if (commandEl) {
|
||
const componentPath = this.getCleanPath();
|
||
commandEl.textContent = `npx claude-code-templates@latest --${this.component.type}=${componentPath} --yes`;
|
||
}
|
||
|
||
// Populate sidebar GitHub link
|
||
const sidebarLink = document.getElementById('githubLinkSidebar');
|
||
if (sidebarLink) {
|
||
sidebarLink.href = this.generateGitHubURL();
|
||
}
|
||
}
|
||
|
||
activateTabFromHash() {
|
||
const hash = window.location.hash.slice(1);
|
||
if (hash) {
|
||
this.switchTab(hash);
|
||
}
|
||
}
|
||
|
||
switchTab(tabName) {
|
||
const validTabs = ['overview', 'code', 'installation'];
|
||
if (!validTabs.includes(tabName)) return;
|
||
|
||
// Update buttons
|
||
document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||
});
|
||
|
||
// Update panes
|
||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||
pane.classList.toggle('active', pane.id === `tab-${tabName}`);
|
||
});
|
||
}
|
||
|
||
renderGitHubLink() {
|
||
const githubUrl = this.generateGitHubURL();
|
||
const githubLinkElement = document.getElementById('githubLink');
|
||
if (githubLinkElement) {
|
||
githubLinkElement.href = githubUrl;
|
||
}
|
||
}
|
||
|
||
generateGitHubURL() {
|
||
let githubUrl = 'https://github.com/davila7/claude-code-templates/';
|
||
|
||
if (this.component.type === 'template') {
|
||
githubUrl += `tree/main/cli-tool/templates/${this.component.folderPath || ''}`;
|
||
} else {
|
||
const componentPath = this.component.path || this.component.name;
|
||
githubUrl += `blob/main/cli-tool/components/${this.component.type}s/${componentPath}`;
|
||
}
|
||
|
||
return githubUrl;
|
||
}
|
||
|
||
|
||
|
||
setupEventListeners() {
|
||
// Track copy command clicks on installation buttons
|
||
document.addEventListener('click', (e) => {
|
||
const copyBtn = e.target.closest('.copy-btn, .quick-copy-btn');
|
||
if (copyBtn && this.component) {
|
||
window.eventTracker?.track('copy_command', {
|
||
component_type: this.component.type,
|
||
component_name: this.component.name,
|
||
source: 'detail_page'
|
||
});
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
document.querySelectorAll('.component-tabs .tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tab = btn.dataset.tab;
|
||
this.switchTab(tab);
|
||
history.replaceState(null, '', `#${tab}`);
|
||
});
|
||
});
|
||
|
||
// Handle hash changes
|
||
window.addEventListener('hashchange', () => {
|
||
this.activateTabFromHash();
|
||
});
|
||
|
||
// Handle browser back/forward buttons
|
||
window.addEventListener('popstate', () => {
|
||
this.loadComponentFromURL();
|
||
});
|
||
|
||
// Handle Add to Stack button
|
||
const addToCartBtn = document.getElementById('addToCartBtn');
|
||
if (addToCartBtn) {
|
||
addToCartBtn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
if (this.component) {
|
||
if (typeof addToCart === 'function') {
|
||
// Convert component type to plural format
|
||
const componentType = this.getComponentTypePlural();
|
||
|
||
if (!componentType) {
|
||
console.error('Failed to get component type plural');
|
||
alert('Unable to determine component type. Please refresh the page.');
|
||
return;
|
||
}
|
||
|
||
if (this.component.name && this.component.path) {
|
||
// Create component object that matches cart manager expectations
|
||
// Clean component name (remove .md extension and format properly)
|
||
const cleanName = this.getCleanComponentName();
|
||
const componentItem = {
|
||
name: cleanName,
|
||
path: this.component.path,
|
||
category: this.component.category
|
||
};
|
||
addToCart(componentItem, componentType);
|
||
} else {
|
||
console.error('Missing component name or path:', this.component);
|
||
alert('Unable to add component: missing component information');
|
||
}
|
||
} else {
|
||
console.error('Cart functionality not available');
|
||
// Fallback: show a message or redirect to main page
|
||
alert('Cart functionality not available. Please return to the main page to add components to your stack.');
|
||
}
|
||
} else {
|
||
console.error('No component data available');
|
||
alert('Component data not loaded. Please refresh the page.');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
getComponentType() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
return urlParams.get('type');
|
||
}
|
||
|
||
getComponentTypePlural() {
|
||
// Get type from the component object itself
|
||
const type = this.component?.type;
|
||
|
||
if (!type) {
|
||
console.error('Component type not found in component object');
|
||
return null;
|
||
}
|
||
|
||
// Convert singular type to plural for cart manager
|
||
const typeMapping = {
|
||
'agent': 'agents',
|
||
'command': 'commands',
|
||
'setting': 'settings',
|
||
'hook': 'hooks',
|
||
'mcp': 'mcps',
|
||
'template': 'templates'
|
||
};
|
||
|
||
return typeMapping[type] || type + 's';
|
||
}
|
||
|
||
getComponentFromURL() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
return urlParams.get('name') || urlParams.get('path');
|
||
}
|
||
|
||
getCleanComponentName() {
|
||
if (!this.component || !this.component.name) {
|
||
return 'Unknown Component';
|
||
}
|
||
|
||
let cleanName = this.component.name;
|
||
|
||
// Remove .md extension if present
|
||
if (cleanName.endsWith('.md')) {
|
||
cleanName = cleanName.slice(0, -3);
|
||
}
|
||
|
||
// Convert kebab-case or snake_case to Title Case
|
||
cleanName = cleanName
|
||
.replace(/[-_]/g, ' ')
|
||
.split(' ')
|
||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||
.join(' ');
|
||
|
||
return cleanName;
|
||
}
|
||
|
||
updatePageMetadata() {
|
||
const cleanName = this.getCleanComponentName(this.component.name);
|
||
const description = this.getComponentDescription();
|
||
const typeCapitalized = this.component.type.charAt(0).toUpperCase() + this.component.type.slice(1);
|
||
const category = this.component.category || 'Development';
|
||
|
||
// Enhanced page title with component type and category
|
||
const pageTitle = `${cleanName} ${typeCapitalized} - Claude Code Templates`;
|
||
const enhancedDescription = `${description} | ${typeCapitalized} for ${category} | Claude Code Templates - AI-powered development tools`;
|
||
|
||
// Generate proper canonical URL for SEO
|
||
const canonicalURL = this.generateCanonicalURL();
|
||
|
||
// Update page title
|
||
document.title = pageTitle;
|
||
const pageTitleElement = document.getElementById('page-title');
|
||
if (pageTitleElement) {
|
||
pageTitleElement.textContent = pageTitle;
|
||
}
|
||
|
||
// Update meta description
|
||
const metaDescription = document.querySelector('meta[name="description"]');
|
||
if (metaDescription) {
|
||
metaDescription.content = enhancedDescription;
|
||
}
|
||
|
||
// Update keywords
|
||
const metaKeywords = document.querySelector('meta[name="keywords"]');
|
||
if (metaKeywords) {
|
||
metaKeywords.content = `${cleanName}, ${this.component.type}, ${category}, Claude Code, AI development, automation, ${this.component.type}s, templates`;
|
||
}
|
||
|
||
// Update canonical URL
|
||
const canonicalLink = document.getElementById('canonical-url');
|
||
if (canonicalLink) {
|
||
canonicalLink.href = canonicalURL;
|
||
}
|
||
|
||
// Update Open Graph meta tags
|
||
const ogUrl = document.getElementById('og-url');
|
||
const ogTitle = document.getElementById('og-title');
|
||
const ogDescription = document.getElementById('og-description');
|
||
|
||
if (ogUrl) ogUrl.content = canonicalURL;
|
||
if (ogTitle) ogTitle.content = pageTitle;
|
||
if (ogDescription) ogDescription.content = enhancedDescription;
|
||
|
||
// Update Twitter meta tags
|
||
const twitterUrl = document.getElementById('twitter-url');
|
||
const twitterTitle = document.getElementById('twitter-title');
|
||
const twitterDescription = document.getElementById('twitter-description');
|
||
|
||
if (twitterUrl) twitterUrl.content = canonicalURL;
|
||
if (twitterTitle) twitterTitle.content = pageTitle;
|
||
if (twitterDescription) twitterDescription.content = enhancedDescription;
|
||
|
||
// Add structured data for better SEO
|
||
this.addStructuredData(cleanName, description, typeCapitalized, category, canonicalURL);
|
||
}
|
||
|
||
generateCanonicalURL() {
|
||
// Generate SEO-friendly canonical URL
|
||
const isLocal = window.location.hostname === 'localhost' ||
|
||
window.location.hostname === '127.0.0.1' ||
|
||
window.location.hostname.includes('5500');
|
||
|
||
if (!isLocal && this.component.type && this.component.name) {
|
||
// Use SEO-friendly URL for production
|
||
let cleanName = this.component.name;
|
||
if (cleanName.endsWith('.md')) {
|
||
cleanName = cleanName.slice(0, -3);
|
||
}
|
||
if (cleanName.endsWith('.json')) {
|
||
cleanName = cleanName.slice(0, -5);
|
||
}
|
||
|
||
const baseUrl = window.location.origin;
|
||
return `${baseUrl}/component/${encodeURIComponent(this.component.type)}/${encodeURIComponent(cleanName)}`;
|
||
}
|
||
|
||
// Fallback to current URL
|
||
return window.location.href;
|
||
}
|
||
|
||
addStructuredData(name, description, type, category, url) {
|
||
// Remove existing structured data
|
||
const existingScript = document.getElementById('component-structured-data');
|
||
if (existingScript) {
|
||
existingScript.remove();
|
||
}
|
||
|
||
// Add new structured data
|
||
const structuredData = {
|
||
"@context": "https://schema.org",
|
||
"@type": "SoftwareApplication",
|
||
"name": name,
|
||
"applicationCategory": "DeveloperApplication",
|
||
"applicationSubCategory": `${type} Component`,
|
||
"description": description,
|
||
"operatingSystem": ["Windows", "macOS", "Linux"],
|
||
"url": url,
|
||
"author": {
|
||
"@type": "Organization",
|
||
"name": "Claude Code Templates Community"
|
||
},
|
||
"offers": {
|
||
"@type": "Offer",
|
||
"price": "0",
|
||
"priceCurrency": "USD"
|
||
},
|
||
"keywords": `${name}, ${type}, ${category}, Claude Code, AI development`,
|
||
"programmingLanguage": type === "command" ? "Shell" : "Configuration",
|
||
"relatedLink": "https://www.anthropic.com/claude-code"
|
||
};
|
||
|
||
const script = document.createElement('script');
|
||
script.id = 'component-structured-data';
|
||
script.type = 'application/ld+json';
|
||
script.textContent = JSON.stringify(structuredData, null, 2);
|
||
document.head.appendChild(script);
|
||
}
|
||
|
||
|
||
// Utility methods
|
||
formatComponentName(name) {
|
||
return name.split('-').map(word =>
|
||
word.charAt(0).toUpperCase() + word.slice(1)
|
||
).join(' ');
|
||
}
|
||
|
||
formatNumber(num) {
|
||
if (num >= 1000000) {
|
||
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
|
||
}
|
||
if (num >= 1000) {
|
||
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
|
||
}
|
||
return num.toString();
|
||
}
|
||
|
||
getComponentDescription() {
|
||
let description = this.component.description || '';
|
||
|
||
if (!description && this.component.content) {
|
||
const descMatch = this.component.content.match(/description:\s*(.+?)(?:\n|$)/);
|
||
if (descMatch) {
|
||
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
||
} else {
|
||
const lines = this.component.content.split('\n');
|
||
const firstParagraph = lines.find(line =>
|
||
line.trim() && !line.startsWith('---') && !line.startsWith('#')
|
||
);
|
||
if (firstParagraph) {
|
||
description = firstParagraph.trim();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!description) {
|
||
description = `A ${this.component.type} component for enhanced development workflow.`;
|
||
}
|
||
|
||
return description;
|
||
}
|
||
|
||
getCleanPath() {
|
||
let componentPath = this.component.path || this.component.name;
|
||
|
||
// Remove file extensions
|
||
if (componentPath.endsWith('.md') || componentPath.endsWith('.json')) {
|
||
componentPath = componentPath.replace(/\.(md|json)$/, '');
|
||
}
|
||
|
||
return componentPath;
|
||
}
|
||
|
||
highlightCode(content, language) {
|
||
// Basic syntax highlighting (same as modal)
|
||
let highlighted = content
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
|
||
if (language === 'markdown' || language === 'yaml') {
|
||
// Highlight YAML/Markdown frontmatter keys (blue)
|
||
highlighted = highlighted.replace(/^([a-zA-Z_-]+):/gm, '<span style="color: #569cd6;">$1</span>:');
|
||
|
||
// Highlight strings in quotes (orange)
|
||
highlighted = highlighted.replace(/"([^&]+?)"/g, '<span style="color: #ce9178;">"$1"</span>');
|
||
|
||
// Highlight important keywords (light blue)
|
||
highlighted = highlighted.replace(/\b(hackathon|strategy|AI|solution|ideation|evaluation|projects|feedback|concepts|feasibility|guidance|agent|specialist|brainstorming|winning|judge|feedback|Context|User|Examples)\b/gi, '<span style="color: #4fc1ff;">$1</span>');
|
||
|
||
// Highlight markdown headers (blue)
|
||
highlighted = highlighted.replace(/^(#+)\s*(.+)$/gm, '<span style="color: #569cd6;">$1</span> <span style="color: #dcdcaa;">$2</span>');
|
||
|
||
// Highlight code in backticks
|
||
highlighted = highlighted.replace(/`([^`]+)`/g, '<span style="color: #ce9178;">$1</span>');
|
||
|
||
// Highlight YAML separators
|
||
highlighted = highlighted.replace(/^---$/gm, '<span style="color: #808080;">---</span>');
|
||
}
|
||
|
||
return highlighted;
|
||
}
|
||
|
||
showError(message) {
|
||
console.error('Component page error:', message);
|
||
|
||
document.getElementById('loadingState').style.display = 'none';
|
||
document.getElementById('componentContent').style.display = 'none';
|
||
document.getElementById('errorState').style.display = 'block';
|
||
|
||
// Update error message if needed
|
||
const errorElement = document.querySelector('#errorState p');
|
||
if (errorElement && message !== 'Missing component parameters in URL') {
|
||
errorElement.textContent = message;
|
||
}
|
||
}
|
||
|
||
// Static method to create URL for component
|
||
static createComponentURL(type, name, path) {
|
||
// Detect if we're in local development
|
||
const isLocal = window.location.hostname === 'localhost' ||
|
||
window.location.hostname === '127.0.0.1' ||
|
||
window.location.hostname.includes('5500');
|
||
|
||
if (!isLocal && type && name) {
|
||
// Use SEO-friendly URL structure for production: /component/type/name
|
||
let cleanName = name;
|
||
if (cleanName.endsWith('.md')) {
|
||
cleanName = cleanName.slice(0, -3);
|
||
}
|
||
if (cleanName.endsWith('.json')) {
|
||
cleanName = cleanName.slice(0, -5);
|
||
}
|
||
|
||
return `component/${encodeURIComponent(type)}/${encodeURIComponent(cleanName)}`;
|
||
}
|
||
|
||
// Use query parameters for local development or fallback
|
||
const params = new URLSearchParams();
|
||
params.set('type', type);
|
||
if (name) params.set('name', name);
|
||
if (path) params.set('path', path);
|
||
|
||
return `component.html?${params.toString()}`;
|
||
}
|
||
}
|
||
|
||
// Global function for creating component URLs (used by other scripts)
|
||
function createComponentURL(type, name, path) {
|
||
return ComponentPageManager.createComponentURL(type, name, path);
|
||
}
|
||
|
||
// Initialize when DOM is loaded
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
window.componentPageManager = new ComponentPageManager();
|
||
});
|
||
|
||
// Export for other scripts
|
||
window.ComponentPageManager = ComponentPageManager;
|
||
|
||
// Component Share functionality
|
||
function toggleComponentShareDropdown() {
|
||
const shareDropdown = document.getElementById('componentShareDropdown');
|
||
if (shareDropdown) {
|
||
shareDropdown.classList.toggle('open');
|
||
}
|
||
}
|
||
|
||
function shareComponentOnTwitter() {
|
||
const componentManager = window.componentPageManager;
|
||
let message;
|
||
|
||
if (componentManager && componentManager.component) {
|
||
const cleanName = componentManager.getCleanComponentName(componentManager.component.name);
|
||
const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1);
|
||
const category = componentManager.component.category || 'Development';
|
||
const canonicalURL = componentManager.generateCanonicalURL();
|
||
|
||
message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code!
|
||
|
||
Perfect for ${category.toLowerCase()} workflows with AI-powered automation.
|
||
|
||
${canonicalURL}
|
||
|
||
#ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`;
|
||
} else {
|
||
// Fallback message
|
||
const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component';
|
||
const currentURL = window.location.href;
|
||
message = `🚀 Check out this ${componentTitle} for Claude Code!
|
||
|
||
Perfect for AI-powered development workflows.
|
||
|
||
${currentURL}
|
||
|
||
#ClaudeCode #AI #Development`;
|
||
}
|
||
|
||
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}`;
|
||
window.open(twitterUrl, '_blank');
|
||
|
||
// Close dropdown after sharing
|
||
const shareDropdown = document.getElementById('componentShareDropdown');
|
||
if (shareDropdown) {
|
||
shareDropdown.classList.remove('open');
|
||
}
|
||
}
|
||
|
||
function shareComponentOnThreads() {
|
||
const componentManager = window.componentPageManager;
|
||
let message;
|
||
|
||
if (componentManager && componentManager.component) {
|
||
const cleanName = componentManager.getCleanComponentName(componentManager.component.name);
|
||
const type = componentManager.component.type.charAt(0).toUpperCase() + componentManager.component.type.slice(1);
|
||
const category = componentManager.component.category || 'Development';
|
||
const canonicalURL = componentManager.generateCanonicalURL();
|
||
|
||
message = `🚀 Found this amazing ${cleanName} ${type} for Claude Code!
|
||
|
||
Perfect for ${category.toLowerCase()} workflows with AI-powered automation.
|
||
|
||
${canonicalURL}
|
||
|
||
#ClaudeCode #AI #Development #${category.replace(/\s+/g, '')} #Automation`;
|
||
} else {
|
||
// Fallback message
|
||
const componentTitle = document.getElementById('componentTitle')?.textContent || 'Claude Code Component';
|
||
const currentURL = window.location.href;
|
||
message = `🚀 Check out this ${componentTitle} for Claude Code!
|
||
|
||
Perfect for AI-powered development workflows.
|
||
|
||
${currentURL}
|
||
|
||
#ClaudeCode #AI #Development`;
|
||
}
|
||
|
||
const threadsUrl = `https://threads.net/intent/post?text=${encodeURIComponent(message)}`;
|
||
window.open(threadsUrl, '_blank');
|
||
|
||
// Close dropdown after sharing
|
||
const shareDropdown = document.getElementById('componentShareDropdown');
|
||
if (shareDropdown) {
|
||
shareDropdown.classList.remove('open');
|
||
}
|
||
}
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
const shareDropdown = document.getElementById('componentShareDropdown');
|
||
if (shareDropdown && !shareDropdown.contains(e.target)) {
|
||
shareDropdown.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Validation Modal System
|
||
window.openValidationModal = function(validatorKey, validatorLabel, validatorData, description) {
|
||
// Create modal overlay
|
||
const modalOverlay = document.createElement('div');
|
||
modalOverlay.className = 'modal-overlay';
|
||
modalOverlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
padding: 20px;
|
||
backdrop-filter: blur(3px);
|
||
overflow: hidden;
|
||
`;
|
||
|
||
// Create modal container
|
||
const modalContainer = document.createElement('div');
|
||
modalContainer.style.cssText = `
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
border-radius: 8px;
|
||
max-width: 700px;
|
||
max-height: 80vh;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||
overflow: hidden;
|
||
`;
|
||
|
||
// Handle both old format (boolean) and new format (object)
|
||
const passed = typeof validatorData === 'boolean' ? validatorData : validatorData.valid;
|
||
const hasErrors = typeof validatorData === 'object' && validatorData.errors && validatorData.errors.length > 0;
|
||
const hasWarnings = typeof validatorData === 'object' && validatorData.warnings && validatorData.warnings.length > 0;
|
||
const hasInfo = typeof validatorData === 'object' && validatorData.info && validatorData.info.length > 0;
|
||
|
||
// Determine status
|
||
let status = 'passed';
|
||
let statusColor = '#48bb78';
|
||
let statusText = 'All Checks Passed';
|
||
let statusIcon = '✓';
|
||
|
||
if (hasErrors) {
|
||
status = 'failed';
|
||
statusColor = '#f56565';
|
||
statusText = 'Validation Failed';
|
||
statusIcon = '✗';
|
||
} else if (hasWarnings) {
|
||
status = 'warning';
|
||
statusColor = '#ed8936';
|
||
statusText = 'Validation Passed with Warnings';
|
||
statusIcon = '⚠';
|
||
}
|
||
|
||
// Build modal content - Header (fixed)
|
||
let modalHeader = `
|
||
<div style="padding: 30px 30px 20px 30px; flex-shrink: 0; border-bottom: 1px solid #333;">
|
||
<!-- Header -->
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px;">
|
||
<div style="flex: 1;">
|
||
<h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">
|
||
${validatorLabel}
|
||
</h2>
|
||
<p style="margin: 0; color: #999; font-size: 14px; line-height: 1.6;">
|
||
${description || 'Validation check details'}
|
||
</p>
|
||
</div>
|
||
<button id="closeValidationModal" style="
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
font-size: 28px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s;
|
||
margin-left: 20px;
|
||
flex-shrink: 0;
|
||
" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#999'">
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Status Badge -->
|
||
<div style="
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 16px;
|
||
background: ${statusColor}20;
|
||
border: 1px solid ${statusColor}40;
|
||
border-radius: 6px;
|
||
color: ${statusColor};
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
">
|
||
<span style="font-size: 16px;">${statusIcon}</span>
|
||
${statusText}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Build modal body content (scrollable)
|
||
let modalBodyContent = '';
|
||
|
||
// Add errors section if present
|
||
if (hasErrors) {
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #f56565; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
||
<span>✗</span>
|
||
Errors (${validatorData.errorCount || validatorData.errors.length})
|
||
</h3>
|
||
<div style="
|
||
background: rgba(245, 101, 101, 0.1);
|
||
border-left: 3px solid #f56565;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
">
|
||
`;
|
||
|
||
validatorData.errors.forEach((error, index) => {
|
||
// Build location info if available
|
||
let locationInfo = '';
|
||
if (error.position || error.line) {
|
||
locationInfo = `
|
||
<div style="
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
padding: 4px 8px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
font-family: 'Courier New', monospace;
|
||
">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,7H13V13H11V7M11,15H13V17H11V15Z"/>
|
||
</svg>
|
||
${error.position ? `Line ${error.position}` : `Line ${error.line}` + (error.column ? `:${error.column}` : '')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Build line text preview if available
|
||
let lineTextPreview = '';
|
||
if (error.lineText) {
|
||
lineTextPreview = `
|
||
<div style="margin-top: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.4); border-left: 2px solid #f56565; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #ddd; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
||
${error.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Handle examples array (for patterns with multiple matches)
|
||
let examplesSection = '';
|
||
if (error.examples && error.examples.length > 0) {
|
||
examplesSection = `
|
||
<div style="margin-top: 12px;">
|
||
<div style="color: #999; font-size: 12px; margin-bottom: 6px; font-weight: 600;">
|
||
Found ${error.examples.length} occurrence${error.examples.length > 1 ? 's' : ''}:
|
||
</div>
|
||
`;
|
||
|
||
error.examples.slice(0, 5).forEach((example, exIdx) => {
|
||
examplesSection += `
|
||
<div style="margin-bottom: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border-radius: 4px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
||
<span style="color: #f56565; font-size: 11px; font-weight: 600; font-family: 'Courier New', monospace;">
|
||
${example.position || `Line ${example.line}`}
|
||
</span>
|
||
${example.text ? `
|
||
<span style="color: #ed8936; font-size: 11px; font-family: 'Courier New', monospace;">
|
||
"${example.text.replace(/</g, '<').replace(/>/g, '>')}"
|
||
</span>
|
||
` : ''}
|
||
</div>
|
||
${example.lineText ? `
|
||
<div style="color: #999; font-size: 11px; font-family: 'Courier New', monospace; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
||
${example.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
if (error.examples.length > 5) {
|
||
examplesSection += `
|
||
<div style="color: #666; font-size: 11px; font-style: italic; margin-top: 4px;">
|
||
... and ${error.examples.length - 5} more occurrence${error.examples.length - 5 > 1 ? 's' : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
examplesSection += `</div>`;
|
||
}
|
||
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: ${index < validatorData.errors.length - 1 ? '12px' : '0'}; padding-bottom: ${index < validatorData.errors.length - 1 ? '12px' : '0'}; border-bottom: ${index < validatorData.errors.length - 1 ? '1px solid rgba(245, 101, 101, 0.2)' : 'none'};">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||
<div style="color: #f56565; font-weight: 600; font-size: 13px;">
|
||
${error.code || 'ERROR'}
|
||
</div>
|
||
${locationInfo}
|
||
</div>
|
||
<div style="color: #ddd; font-size: 16px; line-height: 1.6; margin-bottom: 8px;">
|
||
${error.message || error}
|
||
</div>
|
||
${lineTextPreview}
|
||
${examplesSection}
|
||
${error.context && !error.examples ? `
|
||
<div style="margin-top: 8px; padding: 8px; background: rgba(0, 0, 0, 0.3); border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #999;">
|
||
${JSON.stringify(error.context, null, 2).replace(/\n/g, '<br>').replace(/ /g, ' ')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
modalBodyContent += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add warnings section if present
|
||
if (hasWarnings) {
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #ed8936; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
||
<span>⚠</span>
|
||
Warnings (${validatorData.warningCount || validatorData.warnings.length})
|
||
</h3>
|
||
<div style="
|
||
background: rgba(237, 137, 54, 0.1);
|
||
border-left: 3px solid #ed8936;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
">
|
||
`;
|
||
|
||
validatorData.warnings.forEach((warning, index) => {
|
||
// Build location info if available
|
||
let locationInfo = '';
|
||
if (warning.position || warning.line) {
|
||
locationInfo = `
|
||
<div style="
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
padding: 4px 8px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
font-family: 'Courier New', monospace;
|
||
">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,7H13V13H11V7M11,15H13V17H11V15Z"/>
|
||
</svg>
|
||
${warning.position ? `Line ${warning.position}` : `Line ${warning.line}` + (warning.column ? `:${warning.column}` : '')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Build line text preview if available
|
||
let lineTextPreview = '';
|
||
if (warning.lineText) {
|
||
lineTextPreview = `
|
||
<div style="margin-top: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.4); border-left: 2px solid #ed8936; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #ddd; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
||
${warning.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Handle examples array (for patterns with multiple matches)
|
||
let examplesSection = '';
|
||
if (warning.examples && warning.examples.length > 0) {
|
||
examplesSection = `
|
||
<div style="margin-top: 12px;">
|
||
<div style="color: #999; font-size: 12px; margin-bottom: 6px; font-weight: 600;">
|
||
Found ${warning.examples.length} occurrence${warning.examples.length > 1 ? 's' : ''}:
|
||
</div>
|
||
`;
|
||
|
||
warning.examples.slice(0, 5).forEach((example, exIdx) => {
|
||
examplesSection += `
|
||
<div style="margin-bottom: 8px; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border-radius: 4px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
||
<span style="color: #ed8936; font-size: 11px; font-weight: 600; font-family: 'Courier New', monospace;">
|
||
${example.position || `Line ${example.line}`}
|
||
</span>
|
||
${example.text ? `
|
||
<span style="color: #f9a825; font-size: 11px; font-family: 'Courier New', monospace;">
|
||
"${example.text.replace(/</g, '<').replace(/>/g, '>')}"
|
||
</span>
|
||
` : ''}
|
||
</div>
|
||
${example.lineText ? `
|
||
<div style="color: #999; font-size: 11px; font-family: 'Courier New', monospace; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;">
|
||
${example.lineText.replace(/</g, '<').replace(/>/g, '>')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
if (warning.examples.length > 5) {
|
||
examplesSection += `
|
||
<div style="color: #666; font-size: 11px; font-style: italic; margin-top: 4px;">
|
||
... and ${warning.examples.length - 5} more occurrence${warning.examples.length - 5 > 1 ? 's' : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
examplesSection += `</div>`;
|
||
}
|
||
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: ${index < validatorData.warnings.length - 1 ? '12px' : '0'}; padding-bottom: ${index < validatorData.warnings.length - 1 ? '12px' : '0'}; border-bottom: ${index < validatorData.warnings.length - 1 ? '1px solid rgba(237, 137, 54, 0.2)' : 'none'};">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||
<div style="color: #ed8936; font-weight: 600; font-size: 13px;">
|
||
${warning.code || 'WARNING'}
|
||
</div>
|
||
${locationInfo}
|
||
</div>
|
||
<div style="color: #ddd; font-size: 16px; line-height: 1.6; margin-bottom: 8px;">
|
||
${warning.message || warning}
|
||
</div>
|
||
${lineTextPreview}
|
||
${examplesSection}
|
||
${warning.context && !warning.examples ? `
|
||
<div style="margin-top: 8px; padding: 8px; background: rgba(0, 0, 0, 0.3); border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; color: #999;">
|
||
${JSON.stringify(warning.context, null, 2).replace(/\n/g, '<br>').replace(/ /g, ' ')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
modalBodyContent += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add success/info section if everything passed
|
||
if (!hasErrors && !hasWarnings && (hasInfo || passed)) {
|
||
// Generate validator-specific success summary
|
||
let successSummary = '';
|
||
|
||
switch(validatorKey) {
|
||
case 'structural':
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
||
This component has a valid structure with proper formatting:
|
||
</div>
|
||
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
||
<li>YAML frontmatter is properly formatted and contains all required fields</li>
|
||
<li>File size is within acceptable limits</li>
|
||
<li>UTF-8 encoding is valid with no binary content</li>
|
||
<li>Content structure follows markdown conventions</li>
|
||
<li>Section count is optimal for readability</li>
|
||
</ul>
|
||
`;
|
||
break;
|
||
|
||
case 'integrity':
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
||
This component's integrity has been verified:
|
||
</div>
|
||
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
||
<li>SHA256 hash matches expected value</li>
|
||
<li>No tampering or unauthorized modifications detected</li>
|
||
<li>Version tracking is consistent</li>
|
||
<li>File has not been corrupted during transmission</li>
|
||
</ul>
|
||
`;
|
||
break;
|
||
|
||
case 'semantic':
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
||
This component is safe and contains no malicious content:
|
||
</div>
|
||
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
||
<li>No prompt injection attempts detected</li>
|
||
<li>No dangerous shell commands or system calls</li>
|
||
<li>No obfuscated or suspicious code patterns</li>
|
||
<li>No attempts to access sensitive file paths</li>
|
||
<li>Content follows security best practices</li>
|
||
</ul>
|
||
`;
|
||
break;
|
||
|
||
case 'references':
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
||
All external references have been validated:
|
||
</div>
|
||
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
||
<li>All URLs are properly formatted and accessible</li>
|
||
<li>No SSRF (Server-Side Request Forgery) vulnerabilities</li>
|
||
<li>External links point to trusted domains</li>
|
||
<li>No private network or localhost references</li>
|
||
<li>HTTP links have been upgraded to HTTPS where possible</li>
|
||
</ul>
|
||
`;
|
||
break;
|
||
|
||
case 'provenance':
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6; margin-bottom: 12px;">
|
||
Component origin and authorship have been confirmed:
|
||
</div>
|
||
<ul style="margin: 0; padding-left: 20px; color: #999; font-size: 13px; line-height: 1.8;">
|
||
<li>Author metadata is present and valid</li>
|
||
<li>Repository information is correct</li>
|
||
<li>License information is specified</li>
|
||
<li>Component source can be traced to original repository</li>
|
||
<li>No signs of unauthorized redistribution</li>
|
||
</ul>
|
||
`;
|
||
break;
|
||
|
||
default:
|
||
successSummary = `
|
||
<div style="color: #bbb; font-size: 14px; line-height: 1.6;">
|
||
This component has been validated and meets all ${validatorLabel.toLowerCase()} requirements.
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #48bb78; font-size: 16px; font-weight: 600; margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
|
||
<span>✓</span>
|
||
Validation Passed
|
||
</h3>
|
||
<div style="
|
||
background: rgba(72, 187, 120, 0.1);
|
||
border-left: 3px solid #48bb78;
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
">
|
||
<div style="color: #48bb78; font-weight: 600; font-size: 14px; margin-bottom: 12px;">
|
||
All ${validatorLabel} checks passed successfully
|
||
</div>
|
||
${successSummary}
|
||
`;
|
||
|
||
// Add info items if present
|
||
if (hasInfo) {
|
||
modalBodyContent += `
|
||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(72, 187, 120, 0.2);">
|
||
<div style="color: #48bb78; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
|
||
Additional Details:
|
||
</div>
|
||
`;
|
||
validatorData.info.forEach((info, index) => {
|
||
modalBodyContent += `
|
||
<div style="margin-bottom: ${index < validatorData.info.length - 1 ? '8px' : '0'}; color: #999; font-size: 13px; padding-left: 12px;">
|
||
• ${info.message || info}
|
||
</div>
|
||
`;
|
||
});
|
||
modalBodyContent += `
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
modalBodyContent += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Build modal footer (fixed)
|
||
let modalFooter = `
|
||
<div style="display: flex; justify-content: flex-end; margin-top: 24px; padding-top: 24px; border-top: 1px solid #333;">
|
||
<button id="closeValidationModalBtn" style="
|
||
background: #2d2d2d;
|
||
border: 1px solid #444;
|
||
color: #fff;
|
||
padding: 10px 24px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#3d3d3d'; this.style.borderColor='#555'" onmouseout="this.style.background='#2d2d2d'; this.style.borderColor='#444'">
|
||
Close
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Assemble the complete modal with scrollable body
|
||
modalContainer.innerHTML = `
|
||
${modalHeader}
|
||
<div style="flex: 1; overflow-y: auto; padding: 30px;">
|
||
${modalBodyContent}
|
||
</div>
|
||
<div style="flex-shrink: 0; padding: 20px 30px; border-top: 1px solid #333;">
|
||
${modalFooter}
|
||
</div>
|
||
`;
|
||
modalOverlay.appendChild(modalContainer);
|
||
document.body.appendChild(modalOverlay);
|
||
|
||
// Close modal handlers
|
||
function closeModal() {
|
||
document.body.removeChild(modalOverlay);
|
||
}
|
||
|
||
document.getElementById('closeValidationModal').addEventListener('click', closeModal);
|
||
document.getElementById('closeValidationModalBtn').addEventListener('click', closeModal);
|
||
|
||
// Close on overlay click (but not on modal content click)
|
||
modalOverlay.addEventListener('click', (e) => {
|
||
if (e.target === modalOverlay) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Close on Escape key
|
||
function handleEscape(e) {
|
||
if (e.key === 'Escape') {
|
||
closeModal();
|
||
document.removeEventListener('keydown', handleEscape);
|
||
}
|
||
}
|
||
document.addEventListener('keydown', handleEscape);
|
||
}; |