mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-21 10:55:13 +00:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
/**
|
||||
* Skills page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getRawGitHubUrl } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
import JSZip from '../jszip';
|
||||
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||
import { FuzzySearch, SearchItem } from "../search";
|
||||
import {
|
||||
fetchData,
|
||||
debounce,
|
||||
escapeHtml,
|
||||
getGitHubUrl,
|
||||
getRawGitHubUrl,
|
||||
showToast,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import JSZip from "../jszip";
|
||||
|
||||
interface SkillFile {
|
||||
name: string;
|
||||
@@ -29,86 +36,120 @@ interface SkillsData {
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'skill';
|
||||
const resourceType = "skill";
|
||||
let allItems: Skill[] = [];
|
||||
let search = new FuzzySearch<Skill>();
|
||||
let categorySelect: Choices;
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
hasAssets: false
|
||||
hasAssets: false,
|
||||
};
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const countEl = document.getElementById("results-count");
|
||||
const query = searchInput?.value || "";
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter(item => currentFilters.categories.includes(item.category));
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
if (currentFilters.hasAssets) {
|
||||
results = results.filter(item => item.hasAssets);
|
||||
results = results.filter((item) => item.hasAssets);
|
||||
}
|
||||
|
||||
renderItems(results, query);
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.categories.length > 0) activeFilters.push(`${currentFilters.categories.length} categor${currentFilters.categories.length > 1 ? 'ies' : 'y'}`);
|
||||
if (currentFilters.hasAssets) activeFilters.push('has assets');
|
||||
if (currentFilters.categories.length > 0)
|
||||
activeFilters.push(
|
||||
`${currentFilters.categories.length} categor${
|
||||
currentFilters.categories.length > 1 ? "ies" : "y"
|
||||
}`
|
||||
);
|
||||
if (currentFilters.hasAssets) activeFilters.push("has assets");
|
||||
let countText = `${results.length} of ${allItems.length} skills`;
|
||||
if (activeFilters.length > 0) {
|
||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
||||
countText += ` (filtered by ${activeFilters.join(", ")})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Skill[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
function renderItems(items: Skill[], query = ""): void {
|
||||
const list = document.getElementById("resource-list");
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><h3>No skills found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
list.innerHTML =
|
||||
'<div class="empty-state"><h3>No skills found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="resource-item" data-path="${escapeHtml(item.skillFile)}" data-skill-id="${escapeHtml(item.id)}">
|
||||
list.innerHTML = items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="resource-item" data-path="${escapeHtml(
|
||||
item.skillFile
|
||||
)}" data-skill-id="${escapeHtml(item.id)}">
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">${query ? search.highlight(item.title, query) : escapeHtml(item.title)}</div>
|
||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||
<div class="resource-title">${
|
||||
query ? search.highlight(item.title, query) : escapeHtml(item.title)
|
||||
}</div>
|
||||
<div class="resource-description">${escapeHtml(
|
||||
item.description || "No description"
|
||||
)}</div>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-tag tag-category">${escapeHtml(item.category)}</span>
|
||||
${item.hasAssets ? `<span class="resource-tag tag-assets">${item.assetCount} asset${item.assetCount === 1 ? '' : 's'}</span>` : ''}
|
||||
<span class="resource-tag">${item.files.length} file${item.files.length === 1 ? '' : 's'}</span>
|
||||
<span class="resource-tag tag-category">${escapeHtml(
|
||||
item.category
|
||||
)}</span>
|
||||
${
|
||||
item.hasAssets
|
||||
? `<span class="resource-tag tag-assets">${
|
||||
item.assetCount
|
||||
} asset${item.assetCount === 1 ? "" : "s"}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="resource-tag">${item.files.length} file${
|
||||
item.files.length === 1 ? "" : "s"
|
||||
}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
<button class="btn btn-primary download-skill-btn" data-skill-id="${escapeHtml(item.id)}" title="Download as ZIP">
|
||||
<button class="btn btn-primary download-skill-btn" data-skill-id="${escapeHtml(
|
||||
item.id
|
||||
)}" title="Download as ZIP">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
|
||||
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
<a href="${getGitHubUrl(
|
||||
item.path
|
||||
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add click handlers for opening modal
|
||||
list.querySelectorAll('.resource-item').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
list.querySelectorAll(".resource-item").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
// Don't trigger modal if clicking download button or github link
|
||||
if ((e.target as HTMLElement).closest('.resource-actions')) return;
|
||||
if ((e.target as HTMLElement).closest(".resource-actions")) return;
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, resourceType);
|
||||
});
|
||||
});
|
||||
|
||||
// Add download handlers
|
||||
list.querySelectorAll('.download-skill-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
list.querySelectorAll(".download-skill-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const skillId = (btn as HTMLElement).dataset.skillId;
|
||||
if (skillId) downloadSkill(skillId, btn as HTMLButtonElement);
|
||||
@@ -116,16 +157,20 @@ function renderItems(items: Skill[], query = ''): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<void> {
|
||||
const skill = allItems.find(item => item.id === skillId);
|
||||
async function downloadSkill(
|
||||
skillId: string,
|
||||
btn: HTMLButtonElement
|
||||
): Promise<void> {
|
||||
const skill = allItems.find((item) => item.id === skillId);
|
||||
if (!skill || !skill.files || skill.files.length === 0) {
|
||||
alert('No files found for this skill');
|
||||
showToast("No files found for this skill.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||
btn.innerHTML =
|
||||
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
@@ -152,11 +197,11 @@ async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<v
|
||||
}
|
||||
}
|
||||
|
||||
if (addedFiles === 0) throw new Error('Failed to fetch any files');
|
||||
if (addedFiles === 0) throw new Error("Failed to fetch any files");
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = `${skill.id}.zip`;
|
||||
document.body.appendChild(link);
|
||||
@@ -164,49 +209,75 @@ async function downloadSkill(skillId: string, btn: HTMLButtonElement): Promise<v
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||
setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000);
|
||||
} catch {
|
||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
||||
setTimeout(() => { btn.disabled = false; btn.innerHTML = originalContent; }, 2000);
|
||||
btn.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Download failed.";
|
||||
showToast(message, "error");
|
||||
btn.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initSkillsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const hasAssetsCheckbox = document.getElementById('filter-has-assets') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const list = document.getElementById("resource-list");
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const hasAssetsCheckbox = document.getElementById(
|
||||
"filter-has-assets"
|
||||
) as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
|
||||
const data = await fetchData<SkillsData>('skills.json');
|
||||
const data = await fetchData<SkillsData>("skills.json");
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
if (list)
|
||||
list.innerHTML =
|
||||
'<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
search.setItems(allItems);
|
||||
|
||||
categorySelect = createChoices('#filter-category', { placeholderValue: 'All Categories' });
|
||||
categorySelect.setChoices(data.filters.categories.map(c => ({ value: c, label: c })), 'value', 'label', true);
|
||||
document.getElementById('filter-category')?.addEventListener('change', () => {
|
||||
categorySelect = createChoices("#filter-category", {
|
||||
placeholderValue: "All Categories",
|
||||
});
|
||||
categorySelect.setChoices(
|
||||
data.filters.categories.map((c) => ({ value: c, label: c })),
|
||||
"value",
|
||||
"label",
|
||||
true
|
||||
);
|
||||
document.getElementById("filter-category")?.addEventListener("change", () => {
|
||||
currentFilters.categories = getChoicesValues(categorySelect);
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
);
|
||||
|
||||
hasAssetsCheckbox?.addEventListener('change', () => {
|
||||
hasAssetsCheckbox?.addEventListener("change", () => {
|
||||
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [], hasAssets: false };
|
||||
categorySelect.removeActiveItems();
|
||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (searchInput) searchInput.value = "";
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
@@ -214,4 +285,4 @@ export async function initSkillsPage(): Promise<void> {
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initSkillsPage);
|
||||
document.addEventListener("DOMContentLoaded", initSkillsPage);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Tools page functionality
|
||||
*/
|
||||
import { FuzzySearch, type SearchableItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml } from '../utils';
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, debounce, escapeHtml } from "../utils";
|
||||
|
||||
export interface Tool extends SearchableItem {
|
||||
id: string;
|
||||
@@ -16,8 +16,8 @@ export interface Tool extends SearchableItem {
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
'vscode-insiders'?: string;
|
||||
'visual-studio'?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
@@ -43,19 +43,25 @@ let allItems: Tool[] = [];
|
||||
let search: FuzzySearch<Tool>;
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
query: '',
|
||||
query: "",
|
||||
};
|
||||
|
||||
function formatMultilineText(text: string): string {
|
||||
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
const query = searchInput?.value || '';
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const countEl = document.getElementById("results-count");
|
||||
const query = searchInput?.value || "";
|
||||
currentFilters.query = query;
|
||||
|
||||
let results = query ? search.search(query) : [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter(item =>
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
@@ -69,8 +75,8 @@ function applyFiltersAndRender(): void {
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderTools(tools: Tool[], query = ''): void {
|
||||
const container = document.getElementById('tools-list');
|
||||
function renderTools(tools: Tool[], query = ""): void {
|
||||
const container = document.getElementById("tools-list");
|
||||
if (!container) return;
|
||||
|
||||
if (tools.length === 0) {
|
||||
@@ -83,40 +89,54 @@ function renderTools(tools: Tool[], query = ''): void {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tools.map(tool => {
|
||||
const badges: string[] = [];
|
||||
if (tool.featured) {
|
||||
badges.push('<span class="tool-badge featured">Featured</span>');
|
||||
}
|
||||
badges.push(`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`);
|
||||
container.innerHTML = tools
|
||||
.map((tool) => {
|
||||
const badges: string[] = [];
|
||||
if (tool.featured) {
|
||||
badges.push('<span class="tool-badge featured">Featured</span>');
|
||||
}
|
||||
badges.push(
|
||||
`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`
|
||||
);
|
||||
|
||||
const features = tool.features && tool.features.length > 0
|
||||
? `<div class="tool-section">
|
||||
const features =
|
||||
tool.features && tool.features.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Features</h3>
|
||||
<ul>${tool.features.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>
|
||||
<ul>${tool.features
|
||||
.map((f) => `<li>${escapeHtml(f)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
const requirements = tool.requirements && tool.requirements.length > 0
|
||||
? `<div class="tool-section">
|
||||
const requirements =
|
||||
tool.requirements && tool.requirements.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Requirements</h3>
|
||||
<ul>${tool.requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
|
||||
<ul>${tool.requirements
|
||||
.map((r) => `<li>${escapeHtml(r)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
const tags = tool.tags && tool.tags.length > 0
|
||||
? `<div class="tool-tags">
|
||||
${tool.tags.map(t => `<span class="tool-tag">${escapeHtml(t)}</span>`).join('')}
|
||||
const tags =
|
||||
tool.tags && tool.tags.length > 0
|
||||
? `<div class="tool-tags">
|
||||
${tool.tags
|
||||
.map((t) => `<span class="tool-tag">${escapeHtml(t)}</span>`)
|
||||
.join("")}
|
||||
</div>`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
const config = tool.configuration
|
||||
? `<div class="tool-config">
|
||||
const config = tool.configuration
|
||||
? `<div class="tool-config">
|
||||
<h3>Configuration</h3>
|
||||
<div class="tool-config-wrapper">
|
||||
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
|
||||
</div>
|
||||
<button class="copy-config-btn" data-config="${encodeURIComponent(tool.configuration.content)}">
|
||||
<button class="copy-config-btn" data-config="${encodeURIComponent(
|
||||
tool.configuration.content
|
||||
)}">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
||||
@@ -124,52 +144,74 @@ function renderTools(tools: Tool[], query = ''): void {
|
||||
Copy Configuration
|
||||
</button>
|
||||
</div>`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
const actions: string[] = [];
|
||||
if (tool.links.blog) {
|
||||
actions.push(`<a href="${tool.links.blog}" class="btn btn-secondary" target="_blank" rel="noopener">📖 Blog</a>`);
|
||||
}
|
||||
if (tool.links.marketplace) {
|
||||
actions.push(`<a href="${tool.links.marketplace}" class="btn btn-secondary" target="_blank" rel="noopener">🏪 Marketplace</a>`);
|
||||
}
|
||||
if (tool.links.npm) {
|
||||
actions.push(`<a href="${tool.links.npm}" class="btn btn-secondary" target="_blank" rel="noopener">📦 npm</a>`);
|
||||
}
|
||||
if (tool.links.pypi) {
|
||||
actions.push(`<a href="${tool.links.pypi}" class="btn btn-secondary" target="_blank" rel="noopener">🐍 PyPI</a>`);
|
||||
}
|
||||
if (tool.links.documentation) {
|
||||
actions.push(`<a href="${tool.links.documentation}" class="btn btn-secondary" target="_blank" rel="noopener">📚 Docs</a>`);
|
||||
}
|
||||
if (tool.links.github) {
|
||||
actions.push(`<a href="${tool.links.github}" class="btn btn-secondary" target="_blank" rel="noopener">GitHub</a>`);
|
||||
}
|
||||
if (tool.links.vscode) {
|
||||
actions.push(`<a href="${tool.links.vscode}" class="btn btn-primary" target="_blank" rel="noopener">Install in VS Code</a>`);
|
||||
}
|
||||
if (tool.links['vscode-insiders']) {
|
||||
actions.push(`<a href="${tool.links['vscode-insiders']}" class="btn btn-outline" target="_blank" rel="noopener">VS Code Insiders</a>`);
|
||||
}
|
||||
if (tool.links['visual-studio']) {
|
||||
actions.push(`<a href="${tool.links['visual-studio']}" class="btn btn-outline" target="_blank" rel="noopener">Visual Studio</a>`);
|
||||
}
|
||||
const actions: string[] = [];
|
||||
if (tool.links.blog) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.blog}" class="btn btn-secondary" target="_blank" rel="noopener">📖 Blog</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.marketplace) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.marketplace}" class="btn btn-secondary" target="_blank" rel="noopener">🏪 Marketplace</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.npm) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.npm}" class="btn btn-secondary" target="_blank" rel="noopener">📦 npm</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.pypi) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.pypi}" class="btn btn-secondary" target="_blank" rel="noopener">🐍 PyPI</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.documentation) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.documentation}" class="btn btn-secondary" target="_blank" rel="noopener">📚 Docs</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.github) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.github}" class="btn btn-secondary" target="_blank" rel="noopener">GitHub</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links.vscode) {
|
||||
actions.push(
|
||||
`<a href="${tool.links.vscode}" class="btn btn-primary" target="_blank" rel="noopener">Install in VS Code</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links["vscode-insiders"]) {
|
||||
actions.push(
|
||||
`<a href="${tool.links["vscode-insiders"]}" class="btn btn-outline" target="_blank" rel="noopener">VS Code Insiders</a>`
|
||||
);
|
||||
}
|
||||
if (tool.links["visual-studio"]) {
|
||||
actions.push(
|
||||
`<a href="${tool.links["visual-studio"]}" class="btn btn-outline" target="_blank" rel="noopener">Visual Studio</a>`
|
||||
);
|
||||
}
|
||||
|
||||
const actionsHtml = actions.length > 0
|
||||
? `<div class="tool-actions">${actions.join('')}</div>`
|
||||
: '';
|
||||
const actionsHtml =
|
||||
actions.length > 0
|
||||
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||
: "";
|
||||
|
||||
const titleHtml = query ? search.highlight(tool.name, query) : escapeHtml(tool.name);
|
||||
const titleHtml = query
|
||||
? search.highlight(tool.name, query)
|
||||
: escapeHtml(tool.name);
|
||||
const descriptionHtml = formatMultilineText(tool.description);
|
||||
|
||||
return `
|
||||
return `
|
||||
<div class="tool-card">
|
||||
<div class="tool-header">
|
||||
<h2>${titleHtml}</h2>
|
||||
<div class="tool-badges">
|
||||
${badges.join('')}
|
||||
${badges.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<p class="tool-description">${escapeHtml(tool.description)}</p>
|
||||
<p class="tool-description">${descriptionHtml}</p>
|
||||
${features}
|
||||
${requirements}
|
||||
${config}
|
||||
@@ -177,20 +219,21 @@ function renderTools(tools: Tool[], query = ''): void {
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join("");
|
||||
|
||||
setupCopyConfigHandlers();
|
||||
}
|
||||
|
||||
function setupCopyConfigHandlers(): void {
|
||||
document.querySelectorAll('.copy-config-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
document.querySelectorAll(".copy-config-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
const config = decodeURIComponent(button.dataset.config || '');
|
||||
const config = decodeURIComponent(button.dataset.config || "");
|
||||
try {
|
||||
await navigator.clipboard.writeText(config);
|
||||
button.classList.add('copied');
|
||||
button.classList.add("copied");
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
@@ -199,35 +242,40 @@ function setupCopyConfigHandlers(): void {
|
||||
Copied!
|
||||
`;
|
||||
setTimeout(() => {
|
||||
button.classList.remove('copied');
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = originalHtml;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function initToolsPage(): Promise<void> {
|
||||
const container = document.getElementById('tools-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const categoryFilter = document.getElementById('filter-category') as HTMLSelectElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const countEl = document.getElementById('results-count');
|
||||
const container = document.getElementById("tools-list");
|
||||
const searchInput = document.getElementById(
|
||||
"search-input"
|
||||
) as HTMLInputElement;
|
||||
const categoryFilter = document.getElementById(
|
||||
"filter-category"
|
||||
) as HTMLSelectElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="loading">Loading tools...</div>';
|
||||
}
|
||||
|
||||
const data = await fetchData<ToolsData>('tools.json');
|
||||
const data = await fetchData<ToolsData>("tools.json");
|
||||
if (!data || !data.items) {
|
||||
if (container) container.innerHTML = '<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
||||
if (container)
|
||||
container.innerHTML =
|
||||
'<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Map items to include title for FuzzySearch
|
||||
allItems = data.items.map(item => ({
|
||||
allItems = data.items.map((item) => ({
|
||||
...item,
|
||||
title: item.name, // FuzzySearch uses title
|
||||
}));
|
||||
@@ -237,23 +285,33 @@ export async function initToolsPage(): Promise<void> {
|
||||
|
||||
// Populate category filter
|
||||
if (categoryFilter && data.filters.categories) {
|
||||
categoryFilter.innerHTML = '<option value="">All Categories</option>' +
|
||||
data.filters.categories.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
|
||||
categoryFilter.innerHTML =
|
||||
'<option value="">All Categories</option>' +
|
||||
data.filters.categories
|
||||
.map(
|
||||
(c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
categoryFilter.addEventListener('change', () => {
|
||||
currentFilters.categories = categoryFilter.value ? [categoryFilter.value] : [];
|
||||
categoryFilter.addEventListener("change", () => {
|
||||
currentFilters.categories = categoryFilter.value
|
||||
? [categoryFilter.value]
|
||||
: [];
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
}
|
||||
|
||||
// Search input handler
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
debounce(() => applyFiltersAndRender(), 200)
|
||||
);
|
||||
|
||||
// Clear filters
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { categories: [], query: '' };
|
||||
if (categoryFilter) categoryFilter.value = '';
|
||||
if (searchInput) searchInput.value = '';
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [], query: "" };
|
||||
if (categoryFilter) categoryFilter.value = "";
|
||||
if (searchInput) searchInput.value = "";
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
@@ -261,4 +319,4 @@ export async function initToolsPage(): Promise<void> {
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initToolsPage);
|
||||
document.addEventListener("DOMContentLoaded", initToolsPage);
|
||||
|
||||
Reference in New Issue
Block a user