mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-22 05:14:15 +00:00
Removing hooks, workflows, and tools from website (#2292)
* Removing some features from the home page of the website hooks, agentic workflows, and tools are removed - these are minimally used parts of the website * Removing the pages and their references * removing from the readme * Adding the awesome copilot MCP server to our plugin and showing that in the rendered page * Fix broken docs links and MCP plugin docs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4215785a-7e6e-49c5-abaa-c39b0793a11e
This commit is contained in:
@@ -1,104 +0,0 @@
|
||||
import {
|
||||
escapeHtml,
|
||||
getGitHubUrl,
|
||||
getLastUpdatedHtml,
|
||||
} from "../utils";
|
||||
import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render";
|
||||
|
||||
export interface RenderableHookFile {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RenderableHook {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
readmeFile: string;
|
||||
hooks: string[];
|
||||
tags: string[];
|
||||
assets: string[];
|
||||
files: RenderableHookFile[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export type HookSortOption = "title" | "lastUpdated";
|
||||
|
||||
/**
|
||||
* Build the URL of a hook's dedicated detail page.
|
||||
*/
|
||||
export function getHookDetailUrl(id: string): string {
|
||||
return `/hook/${id}/`;
|
||||
}
|
||||
|
||||
export function sortHooks<T extends RenderableHook>(
|
||||
items: T[],
|
||||
sort: HookSortOption
|
||||
): T[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (sort === "lastUpdated") {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderHooksHtml(items: RenderableHook[]): string {
|
||||
if (items.length === 0) {
|
||||
return renderEmptyStateHtml("No hooks found", "Try adjusting the selected filters.");
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const metaHtml = `
|
||||
${item.hooks
|
||||
.map(
|
||||
(hook) => `<span class="resource-tag tag-hook">${escapeHtml(hook)}</span>`
|
||||
)
|
||||
.join("")}
|
||||
${item.tags
|
||||
.map((tag) => `<span class="resource-tag tag-tag">${escapeHtml(tag)}</span>`)
|
||||
.join("")}
|
||||
${
|
||||
item.assets.length > 0
|
||||
? `<span class="resource-tag tag-assets">${item.assets.length} asset${
|
||||
item.assets.length === 1 ? "" : "s"
|
||||
}</span>`
|
||||
: ""
|
||||
}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
`;
|
||||
|
||||
const actionsHtml = `
|
||||
<button class="btn btn-primary download-hook-btn" data-hook-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>
|
||||
`;
|
||||
|
||||
return renderSharedCardHtml({
|
||||
title: item.title,
|
||||
description: item.description || "No description",
|
||||
href: getHookDetailUrl(item.id),
|
||||
articleAttributes: {
|
||||
"data-path": item.readmeFile,
|
||||
"data-hook-id": item.id,
|
||||
},
|
||||
metaHtml,
|
||||
actionsHtml,
|
||||
});
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* Hooks page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
showToast,
|
||||
downloadZipBundle,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
|
||||
import {
|
||||
renderHooksHtml,
|
||||
sortHooks,
|
||||
type HookSortOption,
|
||||
type RenderableHook,
|
||||
} from './hooks-render';
|
||||
|
||||
interface Hook extends RenderableHook {}
|
||||
|
||||
interface HooksData {
|
||||
items: Hook[];
|
||||
filters: {
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Hook[] = [];
|
||||
let tagSelectEl: HTMLSelectElement | null = null;
|
||||
let currentFilters = {
|
||||
tags: [] as string[],
|
||||
};
|
||||
let currentSort: HookSortOption = 'title';
|
||||
let resourceListHandlersReady = false;
|
||||
|
||||
function sortItems(items: Hook[]): Hook[] {
|
||||
return sortHooks(items, currentSort);
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById('results-count');
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.tags.length > 0) {
|
||||
results = results.filter((item) => item.tags.some((tag) => currentFilters.tags.includes(tag)));
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results);
|
||||
let countText = `${results.length} hook${results.length === 1 ? '' : 's'}`;
|
||||
if (currentFilters.tags.length > 0) {
|
||||
countText = `${results.length} of ${allItems.length} hooks (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Hook[]): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = renderHooksHtml(items);
|
||||
}
|
||||
|
||||
async function downloadHook(hookId: string, btn: HTMLButtonElement): Promise<void> {
|
||||
const hook = allItems.find((item) => item.id === hookId);
|
||||
if (!hook) {
|
||||
showToast('Hook not found.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = [
|
||||
{ name: 'README.md', path: hook.readmeFile },
|
||||
...hook.assets.map((asset) => ({
|
||||
name: asset,
|
||||
path: `${hook.path}/${asset}`,
|
||||
})),
|
||||
];
|
||||
|
||||
if (files.length === 0) {
|
||||
showToast('No files found for this hook.', '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...';
|
||||
|
||||
try {
|
||||
await downloadZipBundle(hook.id, files);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
if (!list || resourceListHandlersReady) return;
|
||||
|
||||
list.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const downloadButton = target.closest('.download-hook-btn') as HTMLButtonElement | null;
|
||||
if (downloadButton) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const hookId = downloadButton.dataset.hookId;
|
||||
if (hookId) downloadHook(hookId, downloadButton);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
resourceListHandlersReady = true;
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: '',
|
||||
hook: [],
|
||||
tag: currentFilters.tags,
|
||||
sort: currentSort === 'title' ? '' : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initHooksPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
|
||||
|
||||
setupResourceListHandlers(list as HTMLElement | null);
|
||||
|
||||
const data = await fetchData<HooksData>('hooks.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
tagSelectEl = document.getElementById('filter-tag') as HTMLSelectElement | null;
|
||||
if (tagSelectEl) {
|
||||
tagSelectEl.innerHTML = '';
|
||||
data.filters.tags.forEach((tag) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tag;
|
||||
option.textContent = tag;
|
||||
tagSelectEl?.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
const initialTags = getQueryParamValues('tag').filter((tag) => data.filters.tags.includes(tag));
|
||||
const initialSort = getQueryParam('sort');
|
||||
|
||||
if (initialTags.length > 0) {
|
||||
currentFilters.tags = initialTags;
|
||||
setSelectValues(tagSelectEl, initialTags);
|
||||
}
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
tagSelectEl?.addEventListener('change', () => {
|
||||
currentFilters.tags = getSelectValues(tagSelectEl);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as HookSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { tags: [] };
|
||||
currentSort = 'title';
|
||||
clearSelectValues(tagSelectEl);
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initHooksPage);
|
||||
@@ -1,203 +0,0 @@
|
||||
import { escapeHtml } from "../utils";
|
||||
|
||||
export interface RenderableTool {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
featured: boolean;
|
||||
requirements: string[];
|
||||
features: string[];
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
};
|
||||
configuration?: {
|
||||
type: string;
|
||||
content: string;
|
||||
} | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export type ToolSortOption = "featured" | "title";
|
||||
|
||||
export function sortTools<T extends RenderableTool>(
|
||||
tools: T[],
|
||||
sort: ToolSortOption
|
||||
): T[] {
|
||||
return [...tools].sort((a, b) => {
|
||||
if (sort === "featured") {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function formatMultilineText(text: string): string {
|
||||
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||
}
|
||||
|
||||
function sanitizeToolUrl(url: string): string {
|
||||
try {
|
||||
const protocol = new URL(url).protocol;
|
||||
if (
|
||||
protocol === "http:" ||
|
||||
protocol === "https:" ||
|
||||
protocol === "vscode:" ||
|
||||
protocol === "vscode-insiders:"
|
||||
) {
|
||||
return escapeHtml(url);
|
||||
}
|
||||
} catch {
|
||||
return "#";
|
||||
}
|
||||
|
||||
return "#";
|
||||
}
|
||||
|
||||
function getToolActionLink(
|
||||
href: string | undefined,
|
||||
label: string,
|
||||
className: string
|
||||
): string {
|
||||
if (!href) return "";
|
||||
return `<a href="${sanitizeToolUrl(
|
||||
href
|
||||
)}" class="${className}" target="_blank" rel="noopener">${label}</a>`;
|
||||
}
|
||||
|
||||
export function renderToolsHtml(
|
||||
tools: RenderableTool[]
|
||||
): string {
|
||||
if (tools.length === 0) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<h3>No tools found</h3>
|
||||
<p>Try a different category or clear the current filters</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return 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">
|
||||
<h3>Features</h3>
|
||||
<ul>${tool.features
|
||||
.map((feature) => `<li>${escapeHtml(feature)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const requirements =
|
||||
tool.requirements && tool.requirements.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Requirements</h3>
|
||||
<ul>${tool.requirements
|
||||
.map((requirement) => `<li>${escapeHtml(requirement)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const tags =
|
||||
tool.tags && tool.tags.length > 0
|
||||
? `<div class="tool-tags">
|
||||
${tool.tags
|
||||
.map((tag) => `<span class="tool-tag">${escapeHtml(tag)}</span>`)
|
||||
.join("")}
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
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
|
||||
)}">
|
||||
<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"/>
|
||||
</svg>
|
||||
Copy Configuration
|
||||
</button>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const actions = [
|
||||
getToolActionLink(tool.links.blog, "📖 Blog", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.marketplace,
|
||||
"🏪 Marketplace",
|
||||
"btn btn-secondary"
|
||||
),
|
||||
getToolActionLink(tool.links.npm, "📦 npm", "btn btn-secondary"),
|
||||
getToolActionLink(tool.links.pypi, "🐍 PyPI", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.documentation,
|
||||
"📚 Docs",
|
||||
"btn btn-secondary"
|
||||
),
|
||||
getToolActionLink(tool.links.github, "GitHub", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.vscode,
|
||||
"Install in VS Code",
|
||||
"btn btn-primary"
|
||||
),
|
||||
getToolActionLink(
|
||||
tool.links["vscode-insiders"],
|
||||
"VS Code Insiders",
|
||||
"btn btn-outline"
|
||||
),
|
||||
getToolActionLink(
|
||||
tool.links["visual-studio"],
|
||||
"Visual Studio",
|
||||
"btn btn-outline"
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
const actionsHtml =
|
||||
actions.length > 0
|
||||
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="tool-card">
|
||||
<div class="tool-header">
|
||||
<h2>${escapeHtml(tool.name)}</h2>
|
||||
<div class="tool-badges">
|
||||
${badges.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<p class="tool-description">${formatMultilineText(tool.description)}</p>
|
||||
${features}
|
||||
${requirements}
|
||||
${config}
|
||||
${tags}
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/**
|
||||
* Tools page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import {
|
||||
renderToolsHtml,
|
||||
sortTools,
|
||||
type ToolSortOption,
|
||||
} from "./tools-render";
|
||||
|
||||
export interface Tool {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
featured: boolean;
|
||||
requirements: string[];
|
||||
features: string[];
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
};
|
||||
configuration?: {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface ToolsData {
|
||||
items: Tool[];
|
||||
filters: {
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Tool[] = [];
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
};
|
||||
let currentSort: ToolSortOption = "featured";
|
||||
let copyHandlersReady = false;
|
||||
let initialized = false;
|
||||
|
||||
function sortItems(items: Tool[]): Tool[] {
|
||||
return sortTools(items, currentSort);
|
||||
}
|
||||
|
||||
function getCountText(resultsCount: number): string {
|
||||
if (currentFilters.categories.length === 0) {
|
||||
return `${resultsCount} tool${resultsCount === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
return `${resultsCount} of ${allItems.length} tools (filtered by ${currentFilters.categories.length} categor${currentFilters.categories.length === 1 ? "y" : "ies"})`;
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById("results-count");
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderTools(results);
|
||||
if (countEl) countEl.textContent = getCountText(results.length);
|
||||
}
|
||||
|
||||
function renderTools(tools: Tool[]): void {
|
||||
const container = document.getElementById("tools-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = renderToolsHtml(tools);
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: "",
|
||||
category: currentFilters.categories,
|
||||
sort: currentSort === "featured" ? "" : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
function setupCopyConfigHandlers(): void {
|
||||
if (copyHandlersReady) return;
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const button = (event.target as HTMLElement).closest(
|
||||
".copy-config-btn"
|
||||
) as HTMLButtonElement | null;
|
||||
if (!button) return;
|
||||
|
||||
event.stopPropagation();
|
||||
const config = decodeURIComponent(button.dataset.config || "");
|
||||
try {
|
||||
await navigator.clipboard.writeText(config);
|
||||
button.classList.add("copied");
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 16 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>
|
||||
Copied!
|
||||
`;
|
||||
setTimeout(() => {
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = originalHtml;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
});
|
||||
|
||||
copyHandlersReady = true;
|
||||
}
|
||||
|
||||
export async function initToolsPage(): Promise<void> {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const categoryFilter = document.getElementById(
|
||||
"filter-category"
|
||||
) as HTMLSelectElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<ToolsData>("tools.json");
|
||||
if (!data || !data.items) {
|
||||
const container = document.getElementById("tools-list");
|
||||
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) => ({
|
||||
...item,
|
||||
title: item.name, // FuzzySearch uses title
|
||||
}));
|
||||
|
||||
// Populate category filter
|
||||
if (categoryFilter && data.filters.categories) {
|
||||
categoryFilter.innerHTML =
|
||||
'<option value="">All Categories</option>' +
|
||||
data.filters.categories
|
||||
.map(
|
||||
(c) => `<option value="${c}">${c}</option>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const initialCategory = getQueryParam("category");
|
||||
if (initialCategory && data.filters.categories.includes(initialCategory)) {
|
||||
currentFilters.categories = [initialCategory];
|
||||
categoryFilter.value = initialCategory;
|
||||
}
|
||||
|
||||
categoryFilter.addEventListener("change", () => {
|
||||
currentFilters.categories = categoryFilter.value
|
||||
? [categoryFilter.value]
|
||||
: [];
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
}
|
||||
|
||||
const initialSort = getQueryParam("sort");
|
||||
if (initialSort === "title") {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as ToolSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
|
||||
// Clear filters
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [] };
|
||||
currentSort = "featured";
|
||||
if (categoryFilter) categoryFilter.value = "";
|
||||
if (sortSelect) sortSelect.value = "featured";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
setupCopyConfigHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", initToolsPage);
|
||||
@@ -1,75 +0,0 @@
|
||||
import {
|
||||
escapeHtml,
|
||||
getActionButtonsHtml,
|
||||
getGitHubUrl,
|
||||
getLastUpdatedHtml,
|
||||
} from '../utils';
|
||||
import { renderEmptyStateHtml, renderSharedCardHtml } from './card-render';
|
||||
|
||||
export interface RenderableWorkflow {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
triggers: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export type WorkflowSortOption = 'title' | 'lastUpdated';
|
||||
|
||||
/**
|
||||
* Build the URL of a workflow's dedicated detail page.
|
||||
*/
|
||||
export function getWorkflowDetailUrl(id: string): string {
|
||||
return `/workflow/${id}/`;
|
||||
}
|
||||
|
||||
export function sortWorkflows<T extends RenderableWorkflow>(
|
||||
items: T[],
|
||||
sort: WorkflowSortOption
|
||||
): T[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (sort === 'lastUpdated') {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderWorkflowsHtml(
|
||||
items: RenderableWorkflow[]
|
||||
): string {
|
||||
if (items.length === 0) {
|
||||
return renderEmptyStateHtml('No workflows found', 'Try adjusting the selected filters.');
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const metaHtml = `
|
||||
${item.triggers
|
||||
.map((trigger) => `<span class="resource-tag tag-trigger">${escapeHtml(trigger)}</span>`)
|
||||
.join('')}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
`;
|
||||
|
||||
const actionsHtml = `
|
||||
${getActionButtonsHtml(item.path)}
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
`;
|
||||
|
||||
return renderSharedCardHtml({
|
||||
title: item.title,
|
||||
description: item.description || 'No description',
|
||||
href: getWorkflowDetailUrl(item.id),
|
||||
articleAttributes: {
|
||||
'data-path': item.path,
|
||||
},
|
||||
metaHtml,
|
||||
actionsHtml,
|
||||
});
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Workflows page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
setupActionHandlers,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
|
||||
import {
|
||||
renderWorkflowsHtml,
|
||||
sortWorkflows,
|
||||
type RenderableWorkflow,
|
||||
type WorkflowSortOption,
|
||||
} from './workflows-render';
|
||||
|
||||
interface Workflow extends RenderableWorkflow {
|
||||
id: string;
|
||||
path: string;
|
||||
triggers: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface WorkflowsData {
|
||||
items: Workflow[];
|
||||
filters: {
|
||||
triggers: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Workflow[] = [];
|
||||
let triggerSelectEl: HTMLSelectElement | null = null;
|
||||
let currentFilters = {
|
||||
triggers: [] as string[],
|
||||
};
|
||||
let currentSort: WorkflowSortOption = 'title';
|
||||
|
||||
function sortItems(items: Workflow[]): Workflow[] {
|
||||
return sortWorkflows(items, currentSort);
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById('results-count');
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.triggers.length > 0) {
|
||||
results = results.filter((item) => item.triggers.some((trigger) => currentFilters.triggers.includes(trigger)));
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results);
|
||||
let countText = `${results.length} workflow${results.length === 1 ? '' : 's'}`;
|
||||
if (currentFilters.triggers.length > 0) {
|
||||
countText = `${results.length} of ${allItems.length} workflows (filtered by ${currentFilters.triggers.length} trigger${currentFilters.triggers.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Workflow[]): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = renderWorkflowsHtml(items);
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: '',
|
||||
trigger: currentFilters.triggers,
|
||||
sort: currentSort === 'title' ? '' : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initWorkflowsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
|
||||
|
||||
const data = await fetchData<WorkflowsData>('workflows.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
triggerSelectEl = document.getElementById('filter-trigger') as HTMLSelectElement | null;
|
||||
if (triggerSelectEl) {
|
||||
triggerSelectEl.innerHTML = '';
|
||||
data.filters.triggers.forEach((trigger) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = trigger;
|
||||
option.textContent = trigger;
|
||||
triggerSelectEl?.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
const initialTriggers = getQueryParamValues('trigger').filter((trigger) => data.filters.triggers.includes(trigger));
|
||||
const initialSort = getQueryParam('sort');
|
||||
|
||||
if (initialTriggers.length > 0) {
|
||||
currentFilters.triggers = initialTriggers;
|
||||
setSelectValues(triggerSelectEl, initialTriggers);
|
||||
}
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
triggerSelectEl?.addEventListener('change', () => {
|
||||
currentFilters.triggers = getSelectValues(triggerSelectEl);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as WorkflowSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { triggers: [] };
|
||||
currentSort = 'title';
|
||||
clearSelectValues(triggerSelectEl);
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initWorkflowsPage);
|
||||
Reference in New Issue
Block a user