/** * Utility functions for the Awesome Copilot website */ const REPO_BASE_URL = 'https://raw.githubusercontent.com/github/awesome-copilot/main'; const REPO_GITHUB_URL = 'https://github.com/github/awesome-copilot/blob/main'; // VS Code install URL configurations const VSCODE_INSTALL_CONFIG: Record = { instructions: { baseUrl: 'https://aka.ms/awesome-copilot/install/instructions', scheme: 'chat-instructions' }, prompt: { baseUrl: 'https://aka.ms/awesome-copilot/install/prompt', scheme: 'chat-prompt' }, agent: { baseUrl: 'https://aka.ms/awesome-copilot/install/agent', scheme: 'chat-agent' }, }; /** * Get the base path for the site */ export function getBasePath(): string { // In Astro, import.meta.env.BASE_URL is available at build time // At runtime, we use a data attribute on the body if (typeof document !== 'undefined') { return document.body.dataset.basePath || '/'; } return '/'; } /** * Fetch JSON data from the data directory */ export async function fetchData(filename: string): Promise { try { const basePath = getBasePath(); const response = await fetch(`${basePath}data/${filename}`); if (!response.ok) throw new Error(`Failed to fetch ${filename}`); return await response.json(); } catch (error) { console.error(`Error fetching ${filename}:`, error); return null; } } /** * Fetch raw file content from GitHub */ export async function fetchFileContent(filePath: string): Promise { try { const response = await fetch(`${REPO_BASE_URL}/${filePath}`); if (!response.ok) throw new Error(`Failed to fetch ${filePath}`); return await response.text(); } catch (error) { console.error(`Error fetching file content:`, error); return null; } } /** * Copy text to clipboard */ export async function copyToClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch { // Fallback for older browsers const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); const success = document.execCommand('copy'); document.body.removeChild(textarea); return success; } } /** * Generate VS Code install URL * @param type - Resource type (agent, prompt, instructions) * @param filePath - Path to the file * @param insiders - Whether to use VS Code Insiders */ export function getVSCodeInstallUrl(type: string, filePath: string, insiders = false): string | null { const config = VSCODE_INSTALL_CONFIG[type]; if (!config) return null; const rawUrl = `${REPO_BASE_URL}/${filePath}`; const vscodeScheme = insiders ? 'vscode-insiders' : 'vscode'; const innerUrl = `${vscodeScheme}:${config.scheme}/install?url=${encodeURIComponent(rawUrl)}`; return `${config.baseUrl}?url=${encodeURIComponent(innerUrl)}`; } /** * Get GitHub URL for a file */ export function getGitHubUrl(filePath: string): string { return `${REPO_GITHUB_URL}/${filePath}`; } /** * Get raw GitHub URL for a file (for fetching content) */ export function getRawGitHubUrl(filePath: string): string { return `${REPO_BASE_URL}/${filePath}`; } /** * Download a file from its path */ export async function downloadFile(filePath: string): Promise { try { const response = await fetch(`${REPO_BASE_URL}/${filePath}`); if (!response.ok) throw new Error('Failed to fetch file'); const content = await response.text(); const filename = filePath.split('/').pop() || 'file.md'; const blob = new Blob([content], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return true; } catch (error) { console.error('Download failed:', error); return false; } } /** * Share/copy link to clipboard (deep link to current page with file hash) */ export async function shareFile(filePath: string): Promise { const deepLinkUrl = `${window.location.origin}${window.location.pathname}#file=${encodeURIComponent(filePath)}`; return copyToClipboard(deepLinkUrl); } /** * Show a toast notification */ export function showToast(message: string, type: 'success' | 'error' = 'success'): void { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } /** * Debounce function for search input */ export function debounce void>( func: T, wait: number ): (...args: Parameters) => void { let timeout: ReturnType; return function executedFunction(...args: Parameters) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Escape HTML to prevent XSS */ export function escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Truncate text with ellipsis */ export function truncate(text: string | undefined, maxLength: number): string { if (!text || text.length <= maxLength) return text || ''; return text.slice(0, maxLength).trim() + '...'; } /** * Get resource type from file path */ export function getResourceType(filePath: string): string { if (filePath.endsWith('.agent.md')) return 'agent'; if (filePath.endsWith('.prompt.md')) return 'prompt'; if (filePath.endsWith('.instructions.md')) return 'instruction'; if (filePath.includes('/skills/') && filePath.endsWith('SKILL.md')) return 'skill'; if (filePath.endsWith('.collection.yml')) return 'collection'; return 'unknown'; } /** * Format a resource type for display */ export function formatResourceType(type: string): string { const labels: Record = { agent: '🤖 Agent', prompt: '🎯 Prompt', instruction: '📋 Instruction', skill: '⚡ Skill', collection: '📦 Collection', }; return labels[type] || type; } /** * Get icon for resource type */ export function getResourceIcon(type: string): string { const icons: Record = { agent: '🤖', prompt: '🎯', instruction: '📋', skill: '⚡', collection: '📦', }; return icons[type] || '📄'; } /** * Generate HTML for install dropdown button */ export function getInstallDropdownHtml(type: string, filePath: string, small = false): string { const vscodeUrl = getVSCodeInstallUrl(type, filePath, false); const insidersUrl = getVSCodeInstallUrl(type, filePath, true); if (!vscodeUrl) return ''; const sizeClass = small ? 'install-dropdown-small' : ''; const uniqueId = `install-${filePath.replace(/[^a-zA-Z0-9]/g, '-')}`; return ` `; } /** * Setup dropdown close handlers for dynamically created dropdowns */ export function setupDropdownCloseHandlers(): void { document.addEventListener('click', (e) => { const target = e.target as HTMLElement; // Close all open dropdowns if clicking outside if (!target.closest('.install-dropdown')) { document.querySelectorAll('.install-dropdown.open').forEach(dropdown => { dropdown.classList.remove('open'); }); } }); } /** * Generate HTML for action buttons (download, share) in list view */ export function getActionButtonsHtml(filePath: string, small = false): string { const btnClass = small ? 'btn-small' : ''; const iconSize = small ? 14 : 16; // Escape backslashes first, then single quotes to prevent breaking out of the JavaScript string literal in the onclick attribute const escapedPath = filePath.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); return ` `; } /** * Setup global action handlers for download and share buttons */ export function setupActionHandlers(): void { // Expose functions globally for inline onclick handlers (window as Window & { __downloadFile?: (path: string) => void; __shareFile?: (path: string) => void }).__downloadFile = async (path: string) => { const success = await downloadFile(path); showToast(success ? 'Download started!' : 'Download failed', success ? 'success' : 'error'); }; (window as Window & { __downloadFile?: (path: string) => void; __shareFile?: (path: string) => void }).__shareFile = async (path: string) => { const success = await shareFile(path); showToast(success ? 'Link copied!' : 'Failed to copy link', success ? 'success' : 'error'); }; }