/** * 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< string, { baseUrl: string; scheme: string } > = { 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 { // Deprecated fallback for older browsers that lack the async clipboard API. 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 { if (dropdownHandlersReady) return; dropdownHandlersReady = true; document.addEventListener( "click", (e) => { const target = e.target as HTMLElement; const dropdown = target.closest( '.install-dropdown[data-install-scope="list"]' ); const toggle = target.closest( ".install-btn-toggle" ) as HTMLButtonElement | null; const menuLink = target.closest( ".install-dropdown-menu a" ) as HTMLAnchorElement | null; if (dropdown) { e.stopPropagation(); if (toggle) { e.preventDefault(); const isOpen = dropdown.classList.toggle("open"); toggle.setAttribute("aria-expanded", String(isOpen)); return; } if (menuLink) { dropdown.classList.remove("open"); const toggleBtn = dropdown.querySelector( ".install-btn-toggle" ); toggleBtn?.setAttribute("aria-expanded", "false"); return; } return; } document .querySelectorAll('.install-dropdown[data-install-scope="list"].open') .forEach((openDropdown) => { openDropdown.classList.remove("open"); const toggleBtn = openDropdown.querySelector( ".install-btn-toggle" ); toggleBtn?.setAttribute("aria-expanded", "false"); }); }, true ); } /** * 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; return ` `; } /** * Setup global action handlers for download and share buttons */ export function setupActionHandlers(): void { if (actionHandlersReady) return; actionHandlersReady = true; document.addEventListener( "click", async (e) => { const target = (e.target as HTMLElement).closest( ".action-download, .action-share" ) as HTMLElement | null; if (!target) return; e.preventDefault(); e.stopPropagation(); const path = target.dataset.path; if (!path) return; if (target.classList.contains("action-download")) { const success = await downloadFile(path); showToast( success ? "Download started!" : "Download failed", success ? "success" : "error" ); return; } const success = await shareFile(path); showToast( success ? "Link copied!" : "Failed to copy link", success ? "success" : "error" ); }, true ); } let dropdownHandlersReady = false; let actionHandlersReady = false;