Refactor code for consistency and readability

- Standardized string quotes to double quotes across multiple files.
- Improved formatting and indentation for better readability.
- Added a function to format multiline text in tools rendering.
- Enhanced dropdown and action button handlers for better event management.
- Updated the theme application logic to initialize on page load.
- Refactored utility functions for consistency and clarity.
- Improved error handling and user feedback in download and share functionalities.
This commit is contained in:
Aaron Powell
2026-02-02 16:42:22 +11:00
parent cdb056e44f
commit a1da290d10
12 changed files with 1225 additions and 651 deletions

View File

@@ -2,22 +2,26 @@
* 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';
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'
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'
prompt: {
baseUrl: "https://aka.ms/awesome-copilot/install/prompt",
scheme: "chat-prompt",
},
agent: {
baseUrl: 'https://aka.ms/awesome-copilot/install/agent',
scheme: 'chat-agent'
agent: {
baseUrl: "https://aka.ms/awesome-copilot/install/agent",
scheme: "chat-agent",
},
};
@@ -27,16 +31,18 @@ const VSCODE_INSTALL_CONFIG: Record<string, { baseUrl: string; scheme: string }>
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 || '/';
if (typeof document !== "undefined") {
return document.body.dataset.basePath || "/";
}
return '/';
return "/";
}
/**
* Fetch JSON data from the data directory
*/
export async function fetchData<T = unknown>(filename: string): Promise<T | null> {
export async function fetchData<T = unknown>(
filename: string
): Promise<T | null> {
try {
const basePath = getBasePath();
const response = await fetch(`${basePath}data/${filename}`);
@@ -51,7 +57,9 @@ export async function fetchData<T = unknown>(filename: string): Promise<T | null
/**
* Fetch raw file content from GitHub
*/
export async function fetchFileContent(filePath: string): Promise<string | null> {
export async function fetchFileContent(
filePath: string
): Promise<string | null> {
try {
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
if (!response.ok) throw new Error(`Failed to fetch ${filePath}`);
@@ -70,14 +78,14 @@ export async function copyToClipboard(text: string): Promise<boolean> {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fallback for older browsers
const textarea = document.createElement('textarea');
// 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';
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
const success = document.execCommand("copy");
document.body.removeChild(textarea);
return success;
}
@@ -89,14 +97,20 @@ export async function copyToClipboard(text: string): Promise<boolean> {
* @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 {
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)}`;
const vscodeScheme = insiders ? "vscode-insiders" : "vscode";
const innerUrl = `${vscodeScheme}:${
config.scheme
}/install?url=${encodeURIComponent(rawUrl)}`;
return `${config.baseUrl}?url=${encodeURIComponent(innerUrl)}`;
}
@@ -120,25 +134,25 @@ export function getRawGitHubUrl(filePath: string): string {
export async function downloadFile(filePath: string): Promise<boolean> {
try {
const response = await fetch(`${REPO_BASE_URL}/${filePath}`);
if (!response.ok) throw new Error('Failed to fetch file');
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 filename = filePath.split("/").pop() || "file.md";
const blob = new Blob([content], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
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);
console.error("Download failed:", error);
return false;
}
}
@@ -147,22 +161,27 @@ export async function downloadFile(filePath: string): Promise<boolean> {
* Share/copy link to clipboard (deep link to current page with file hash)
*/
export async function shareFile(filePath: string): Promise<boolean> {
const deepLinkUrl = `${window.location.origin}${window.location.pathname}#file=${encodeURIComponent(filePath)}`;
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');
export function showToast(
message: string,
type: "success" | "error" = "success"
): void {
const existing = document.querySelector(".toast");
if (existing) existing.remove();
const toast = document.createElement('div');
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
@@ -190,7 +209,7 @@ export function debounce<T extends (...args: unknown[]) => void>(
* Escape HTML to prevent XSS
*/
export function escapeHtml(text: string): string {
const div = document.createElement('div');
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
@@ -199,20 +218,21 @@ export function escapeHtml(text: string): string {
* 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() + '...';
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';
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";
}
/**
@@ -220,11 +240,11 @@ export function getResourceType(filePath: string): string {
*/
export function formatResourceType(type: string): string {
const labels: Record<string, string> = {
agent: '🤖 Agent',
prompt: '🎯 Prompt',
instruction: '📋 Instruction',
skill: '⚡ Skill',
collection: '📦 Collection',
agent: "🤖 Agent",
prompt: "🎯 Prompt",
instruction: "📋 Instruction",
skill: "⚡ Skill",
collection: "📦 Collection",
};
return labels[type] || type;
}
@@ -234,42 +254,50 @@ export function formatResourceType(type: string): string {
*/
export function getResourceIcon(type: string): string {
const icons: Record<string, string> = {
agent: '🤖',
prompt: '🎯',
instruction: '📋',
skill: '⚡',
collection: '📦',
agent: "🤖",
prompt: "🎯",
instruction: "📋",
skill: "⚡",
collection: "📦",
};
return icons[type] || '📄';
return icons[type] || "📄";
}
/**
* Generate HTML for install dropdown button
*/
export function getInstallDropdownHtml(type: string, filePath: string, small = false): string {
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, '-')}`;
if (!vscodeUrl) return "";
const sizeClass = small ? "install-dropdown-small" : "";
const uniqueId = `install-${filePath.replace(/[^a-zA-Z0-9]/g, "-")}`;
return `
<div class="install-dropdown ${sizeClass}" id="${uniqueId}" onclick="event.stopPropagation()">
<a href="${vscodeUrl}" class="btn btn-primary ${small ? 'btn-small' : ''} install-btn-main" target="_blank" rel="noopener">
<div class="install-dropdown ${sizeClass}" id="${uniqueId}" data-install-scope="list">
<a href="${vscodeUrl}" class="btn btn-primary ${
small ? "btn-small" : ""
} install-btn-main" target="_blank" rel="noopener">
Install
</a>
<button type="button" class="btn btn-primary ${small ? 'btn-small' : ''} install-btn-toggle" aria-label="Install options" onclick="event.preventDefault(); this.parentElement.classList.toggle('open');">
<button type="button" class="btn btn-primary ${
small ? "btn-small" : ""
} install-btn-toggle" aria-label="Install options" aria-expanded="false">
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor">
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
</svg>
</button>
<div class="install-dropdown-menu">
<a href="${vscodeUrl}" target="_blank" rel="noopener" onclick="this.closest('.install-dropdown').classList.remove('open')">
<a href="${vscodeUrl}" target="_blank" rel="noopener">
VS Code
</a>
<a href="${insidersUrl}" target="_blank" rel="noopener" onclick="this.closest('.install-dropdown').classList.remove('open')">
<a href="${insidersUrl}" target="_blank" rel="noopener">
VS Code Insiders
</a>
</div>
@@ -281,33 +309,77 @@ export function getInstallDropdownHtml(type: string, filePath: string, small = f
* 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');
});
}
});
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<HTMLButtonElement>(
".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<HTMLButtonElement>(
".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 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 `
<button class="btn btn-secondary ${btnClass} action-download" data-path="${escapeHtml(filePath)}" onclick="event.stopPropagation(); window.__downloadFile && window.__downloadFile('${escapedPath}')" title="Download file">
<button class="btn btn-secondary ${btnClass} action-download" data-path="${escapeHtml(
filePath
)}" title="Download file">
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
<path d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"/>
</svg>
</button>
<button class="btn btn-secondary ${btnClass} action-share" data-path="${escapeHtml(filePath)}" onclick="event.stopPropagation(); window.__shareFile && window.__shareFile('${escapedPath}')" title="Copy link">
<button class="btn btn-secondary ${btnClass} action-share" data-path="${escapeHtml(
filePath
)}" title="Copy link">
<svg viewBox="0 0 16 16" width="${iconSize}" height="${iconSize}" fill="currentColor">
<path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"/>
</svg>
@@ -319,14 +391,41 @@ export function getActionButtonsHtml(filePath: string, small = false): string {
* 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');
};
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;