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
+172 -105
View File
@@ -2,9 +2,9 @@
* Samples/Cookbook page functionality
*/
import { FuzzySearch, type SearchableItem } from '../search';
import { fetchData, escapeHtml } from '../utils';
import { createChoices, getChoicesValues, type Choices } from '../choices';
import { FuzzySearch, type SearchableItem } from "../search";
import { fetchData, escapeHtml } from "../utils";
import { createChoices, getChoicesValues, type Choices } from "../choices";
// Types
interface Language {
@@ -59,36 +59,44 @@ let tagChoices: Choices | null = null;
* Initialize the samples page
*/
export async function initSamplesPage(): Promise<void> {
// Load samples data
samplesData = await fetchData<SamplesData>('samples.json');
if (!samplesData || samplesData.cookbooks.length === 0) {
try {
// Load samples data
samplesData = await fetchData<SamplesData>("samples.json");
if (!samplesData || samplesData.cookbooks.length === 0) {
showEmptyState();
return;
}
// Initialize search with all recipes
const allRecipes = samplesData.cookbooks.flatMap((cookbook) =>
cookbook.recipes.map(
(recipe) =>
({
...recipe,
title: recipe.name,
cookbookId: cookbook.id,
} as SearchableItem & { cookbookId: string })
)
);
search = new FuzzySearch(allRecipes);
// Setup UI
setupFilters();
setupSearch();
renderCookbooks();
updateResultsCount();
} catch (error) {
console.error("Failed to initialize samples page:", error);
showEmptyState();
return;
}
// Initialize search with all recipes
const allRecipes = samplesData.cookbooks.flatMap(cookbook =>
cookbook.recipes.map(recipe => ({
...recipe,
title: recipe.name,
cookbookId: cookbook.id
} as SearchableItem & { cookbookId: string }))
);
search = new FuzzySearch(allRecipes);
// Setup UI
setupFilters();
setupSearch();
renderCookbooks();
updateResultsCount();
}
/**
* Show empty state when no cookbooks are available
*/
function showEmptyState(): void {
const container = document.getElementById('samples-list');
const container = document.getElementById("samples-list");
if (container) {
container.innerHTML = `
<div class="empty-state">
@@ -97,10 +105,10 @@ function showEmptyState(): void {
</div>
`;
}
// Hide filters
const filtersBar = document.getElementById('filters-bar');
if (filtersBar) filtersBar.style.display = 'none';
const filtersBar = document.getElementById("filters-bar");
if (filtersBar) filtersBar.style.display = "none";
}
/**
@@ -110,12 +118,14 @@ function setupFilters(): void {
if (!samplesData) return;
// Language filter
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
const languageSelect = document.getElementById(
"filter-language"
) as HTMLSelectElement;
if (languageSelect) {
// Get unique languages across all cookbooks
const languages = new Map<string, Language>();
samplesData.cookbooks.forEach(cookbook => {
cookbook.languages.forEach(lang => {
samplesData.cookbooks.forEach((cookbook) => {
cookbook.languages.forEach((lang) => {
if (!languages.has(lang.id)) {
languages.set(lang.id, lang);
}
@@ -124,13 +134,13 @@ function setupFilters(): void {
languageSelect.innerHTML = '<option value="">All Languages</option>';
languages.forEach((lang, id) => {
const option = document.createElement('option');
const option = document.createElement("option");
option.value = id;
option.textContent = `${lang.icon} ${lang.name}`;
languageSelect.appendChild(option);
});
languageSelect.addEventListener('change', () => {
languageSelect.addEventListener("change", () => {
selectedLanguage = languageSelect.value || null;
renderCookbooks();
updateResultsCount();
@@ -138,18 +148,18 @@ function setupFilters(): void {
}
// Tag filter (multi-select with Choices.js)
const tagSelect = document.getElementById('filter-tag') as HTMLSelectElement;
const tagSelect = document.getElementById("filter-tag") as HTMLSelectElement;
if (tagSelect && samplesData.filters.tags.length > 0) {
// Initialize Choices.js
tagChoices = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
tagChoices = createChoices("#filter-tag", { placeholderValue: "All Tags" });
tagChoices.setChoices(
samplesData.filters.tags.map(tag => ({ value: tag, label: tag })),
'value',
'label',
samplesData.filters.tags.map((tag) => ({ value: tag, label: tag })),
"value",
"label",
true
);
tagSelect.addEventListener('change', () => {
tagSelect.addEventListener("change", () => {
selectedTags = getChoicesValues(tagChoices!);
renderCookbooks();
updateResultsCount();
@@ -157,19 +167,21 @@ function setupFilters(): void {
}
// Clear filters button
const clearBtn = document.getElementById('clear-filters');
clearBtn?.addEventListener('click', clearFilters);
const clearBtn = document.getElementById("clear-filters");
clearBtn?.addEventListener("click", clearFilters);
}
/**
* Setup search functionality
*/
function setupSearch(): void {
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const searchInput = document.getElementById(
"search-input"
) as HTMLInputElement;
if (!searchInput) return;
let debounceTimer: number;
searchInput.addEventListener('input', () => {
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(() => {
renderCookbooks();
@@ -185,16 +197,20 @@ function clearFilters(): void {
selectedLanguage = null;
selectedTags = [];
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
if (languageSelect) languageSelect.value = '';
const languageSelect = document.getElementById(
"filter-language"
) as HTMLSelectElement;
if (languageSelect) languageSelect.value = "";
// Clear Choices.js selection
if (tagChoices) {
tagChoices.removeActiveItems();
}
const searchInput = document.getElementById('search-input') as HTMLInputElement;
if (searchInput) searchInput.value = '';
const searchInput = document.getElementById(
"search-input"
) as HTMLInputElement;
if (searchInput) searchInput.value = "";
renderCookbooks();
updateResultsCount();
@@ -203,44 +219,53 @@ function clearFilters(): void {
/**
* Get filtered recipes
*/
function getFilteredRecipes(): { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] {
function getFilteredRecipes(): {
cookbook: Cookbook;
recipe: Recipe;
highlighted?: string;
}[] {
if (!samplesData || !search) return [];
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const query = searchInput?.value.trim() || '';
const searchInput = document.getElementById(
"search-input"
) as HTMLInputElement;
const query = searchInput?.value.trim() || "";
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] = [];
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] =
[];
if (query) {
// Use fuzzy search - returns SearchableItem[] directly
const searchResults = search.search(query);
results = searchResults.map(item => {
results = searchResults.map((item) => {
const recipe = item as SearchableItem & { cookbookId: string };
const cookbook = samplesData!.cookbooks.find(c => c.id === recipe.cookbookId)!;
const cookbook = samplesData!.cookbooks.find(
(c) => c.id === recipe.cookbookId
)!;
return {
cookbook,
recipe: recipe as unknown as Recipe,
highlighted: search!.highlight(recipe.title, query)
highlighted: search!.highlight(recipe.title, query),
};
});
} else {
// No search query - return all recipes
results = samplesData.cookbooks.flatMap(cookbook =>
cookbook.recipes.map(recipe => ({ cookbook, recipe }))
results = samplesData.cookbooks.flatMap((cookbook) =>
cookbook.recipes.map((recipe) => ({ cookbook, recipe }))
);
}
// Apply language filter
if (selectedLanguage) {
results = results.filter(({ recipe }) =>
recipe.variants[selectedLanguage!]
results = results.filter(
({ recipe }) => recipe.variants[selectedLanguage!]
);
}
// Apply tag filter
if (selectedTags.length > 0) {
results = results.filter(({ recipe }) =>
selectedTags.some(tag => recipe.tags.includes(tag))
selectedTags.some((tag) => recipe.tags.includes(tag))
);
}
@@ -251,7 +276,7 @@ function getFilteredRecipes(): { cookbook: Cookbook; recipe: Recipe; highlighted
* Render cookbooks and recipes
*/
function renderCookbooks(): void {
const container = document.getElementById('samples-list');
const container = document.getElementById("samples-list");
if (!container || !samplesData) return;
const filteredResults = getFilteredRecipes();
@@ -267,7 +292,10 @@ function renderCookbooks(): void {
}
// Group by cookbook
const byCookbook = new Map<string, { cookbook: Cookbook; recipes: { recipe: Recipe; highlighted?: string }[] }>();
const byCookbook = new Map<
string,
{ cookbook: Cookbook; recipes: { recipe: Recipe; highlighted?: string }[] }
>();
filteredResults.forEach(({ cookbook, recipe, highlighted }) => {
if (!byCookbook.has(cookbook.id)) {
byCookbook.set(cookbook.id, { cookbook, recipes: [] });
@@ -275,7 +303,7 @@ function renderCookbooks(): void {
byCookbook.get(cookbook.id)!.recipes.push({ recipe, highlighted });
});
let html = '';
let html = "";
byCookbook.forEach(({ cookbook, recipes }) => {
html += renderCookbookSection(cookbook, recipes);
});
@@ -289,18 +317,27 @@ function renderCookbooks(): void {
/**
* Render a cookbook section
*/
function renderCookbookSection(cookbook: Cookbook, recipes: { recipe: Recipe; highlighted?: string }[]): string {
const languageTabs = cookbook.languages.map(lang => `
<button class="lang-tab${selectedLanguage === lang.id ? ' active' : ''}"
data-lang="${lang.id}"
function renderCookbookSection(
cookbook: Cookbook,
recipes: { recipe: Recipe; highlighted?: string }[]
): string {
const languageTabs = cookbook.languages
.map(
(lang) => `
<button class="lang-tab${selectedLanguage === lang.id ? " active" : ""}"
data-lang="${lang.id}"
title="${lang.name}">
${lang.icon}
</button>
`).join('');
`
)
.join("");
const recipeCards = recipes.map(({ recipe, highlighted }) =>
renderRecipeCard(cookbook, recipe, highlighted)
).join('');
const recipeCards = recipes
.map(({ recipe, highlighted }) =>
renderRecipeCard(cookbook, recipe, highlighted)
)
.join("");
return `
<div class="cookbook-section" data-cookbook="${cookbook.id}">
@@ -323,25 +360,36 @@ function renderCookbookSection(cookbook: Cookbook, recipes: { recipe: Recipe; hi
/**
* Render a recipe card
*/
function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?: string): string {
function renderRecipeCard(
cookbook: Cookbook,
recipe: Recipe,
highlightedName?: string
): string {
const recipeKey = `${cookbook.id}-${recipe.id}`;
const isExpanded = expandedRecipes.has(recipeKey);
// Determine which language to show
const displayLang = selectedLanguage || cookbook.languages[0]?.id || 'nodejs';
const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs";
const variant = recipe.variants[displayLang];
const tags = recipe.tags.map(tag =>
`<span class="recipe-tag">${escapeHtml(tag)}</span>`
).join('');
const tags = recipe.tags
.map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`)
.join("");
const langIndicators = cookbook.languages
.filter(lang => recipe.variants[lang.id])
.map(lang => `<span class="lang-indicator" title="${lang.name}">${lang.icon}</span>`)
.join('');
.filter((lang) => recipe.variants[lang.id])
.map(
(lang) =>
`<span class="lang-indicator" title="${lang.name}">${lang.icon}</span>`
)
.join("");
return `
<div class="recipe-card${isExpanded ? ' expanded' : ''}" data-recipe="${recipeKey}" data-cookbook="${cookbook.id}" data-recipe-id="${recipe.id}">
<div class="recipe-card${
isExpanded ? " expanded" : ""
}" data-recipe="${recipeKey}" data-cookbook="${
cookbook.id
}" data-recipe-id="${recipe.id}">
<div class="recipe-header">
<h3>${highlightedName || escapeHtml(recipe.name)}</h3>
<div class="recipe-langs">${langIndicators}</div>
@@ -349,29 +397,41 @@ function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?:
<p class="recipe-description">${escapeHtml(recipe.description)}</p>
<div class="recipe-tags">${tags}</div>
<div class="recipe-actions">
${variant ? `
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${variant.doc}">
${
variant
? `
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${
variant.doc
}">
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
</svg>
View Recipe
</button>
${variant.example ? `
${
variant.example
? `
<button class="btn btn-secondary btn-small view-example-btn" data-example="${variant.example}">
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
</svg>
View Example
</button>
` : ''}
<a href="https://github.com/github/awesome-copilot/blob/main/${variant.doc}"
`
: ""
}
<a href="https://github.com/github/awesome-copilot/blob/main/${
variant.doc
}"
class="btn btn-secondary btn-small" target="_blank" rel="noopener">
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
GitHub
</a>
` : '<span class="no-variant">Not available for selected language</span>'}
`
: '<span class="no-variant">Not available for selected language</span>'
}
</div>
</div>
`;
@@ -382,35 +442,37 @@ function renderRecipeCard(cookbook: Cookbook, recipe: Recipe, highlightedName?:
*/
function setupRecipeListeners(): void {
// View recipe buttons
document.querySelectorAll('.view-recipe-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
document.querySelectorAll(".view-recipe-btn").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.stopPropagation();
const docPath = (btn as HTMLElement).dataset.doc;
if (docPath) {
await showRecipeContent(docPath, 'recipe');
await showRecipeContent(docPath, "recipe");
}
});
});
// View example buttons
document.querySelectorAll('.view-example-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
document.querySelectorAll(".view-example-btn").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.stopPropagation();
const examplePath = (btn as HTMLElement).dataset.example;
if (examplePath) {
await showRecipeContent(examplePath, 'example');
await showRecipeContent(examplePath, "example");
}
});
});
// Language tab clicks
document.querySelectorAll('.lang-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll(".lang-tab").forEach((tab) => {
tab.addEventListener("click", (e) => {
const langId = (tab as HTMLElement).dataset.lang;
if (langId) {
selectedLanguage = langId;
// Update language filter select
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
const languageSelect = document.getElementById(
"filter-language"
) as HTMLSelectElement;
if (languageSelect) languageSelect.value = langId;
renderCookbooks();
updateResultsCount();
@@ -422,9 +484,12 @@ function setupRecipeListeners(): void {
/**
* Show recipe/example content in modal
*/
async function showRecipeContent(filePath: string, type: 'recipe' | 'example'): Promise<void> {
async function showRecipeContent(
filePath: string,
type: "recipe" | "example"
): Promise<void> {
// Use existing modal infrastructure
const { openFileModal } = await import('../modal');
const { openFileModal } = await import("../modal");
await openFileModal(filePath, type);
}
@@ -432,23 +497,25 @@ async function showRecipeContent(filePath: string, type: 'recipe' | 'example'):
* Update results count display
*/
function updateResultsCount(): void {
const resultsCount = document.getElementById('results-count');
const resultsCount = document.getElementById("results-count");
if (!resultsCount || !samplesData) return;
const filtered = getFilteredRecipes();
const total = samplesData.totalRecipes;
if (filtered.length === total) {
resultsCount.textContent = `${total} recipe${total !== 1 ? 's' : ''}`;
resultsCount.textContent = `${total} recipe${total !== 1 ? "s" : ""}`;
} else {
resultsCount.textContent = `${filtered.length} of ${total} recipe${total !== 1 ? 's' : ''}`;
resultsCount.textContent = `${filtered.length} of ${total} recipe${
total !== 1 ? "s" : ""
}`;
}
}
// Auto-initialize when DOM is ready
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initSamplesPage());
if (typeof document !== "undefined") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initSamplesPage());
} else {
initSamplesPage();
}