mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 18:35:14 +00:00
feat(website): add samples/cookbook page with recipe browser
Integrates the cookbook/ folder into the website's Samples page: Data Structure: - Add cookbook/cookbook.yml manifest defining cookbooks and recipes - Add .schemas/cookbook.schema.json for validation - Add COOKBOOK_DIR constant to eng/constants.mjs Build Integration: - Add generateSamplesData() to generate samples.json from cookbook.yml - Include recipe variants with file paths for each language - Add samples count to manifest.json Website UI: - Create samples.ts with FuzzySearch, language/tag filtering - Replace placeholder samples.astro with functional recipe browser - Recipe cards with language indicators and action buttons - Language tabs for switching between implementations - View Recipe/View Example buttons open modal - GitHub link for each recipe Features: - Search recipes by name/description - Filter by programming language (Node.js, Python, .NET, Go) - Filter by tags (multi-select with Choices.js) - 5 recipes across 4 languages = 20 recipe variants
This commit is contained in:
@@ -1,95 +1,246 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<BaseLayout title="Samples" description="Code samples and examples for building with GitHub Copilot" activeNav="samples">
|
||||
<BaseLayout title="Samples" description="Code samples, recipes, and examples for building with GitHub Copilot" activeNav="samples">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>📚 Samples</h1>
|
||||
<p>Code samples and examples for building with GitHub Copilot</p>
|
||||
<h1>📚 Samples & Recipes</h1>
|
||||
<p>Code samples, recipes, and examples for building with GitHub Copilot tools</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="coming-soon">
|
||||
<div class="coming-soon-icon" aria-hidden="true">🚧</div>
|
||||
<h2>Coming Soon</h2>
|
||||
<p>We're migrating code samples from the <a href="https://github.com/github/copilot-sdk/tree/main/cookbook" target="_blank" rel="noopener">Copilot SDK Cookbook</a> to this repository.</p>
|
||||
<p>Check back soon for examples including:</p>
|
||||
<ul class="sample-list">
|
||||
<li>Building custom agents</li>
|
||||
<li>Integrating with MCP servers</li>
|
||||
<li>Creating prompt templates</li>
|
||||
<li>Working with Copilot APIs</li>
|
||||
</ul>
|
||||
<div class="coming-soon-actions">
|
||||
<a href="https://github.com/github/copilot-sdk/tree/main/cookbook" class="btn btn-primary" target="_blank" rel="noopener">
|
||||
View Current Cookbook
|
||||
</a>
|
||||
<a href="https://github.com/github/awesome-copilot" class="btn btn-secondary" target="_blank" rel="noopener">
|
||||
Watch Repository
|
||||
</a>
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search recipes</label>
|
||||
<input type="text" id="search-input" placeholder="Search recipes..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-language">Language:</label>
|
||||
<select id="filter-language" aria-label="Filter by language">
|
||||
<option value="">All Languages</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="filter-tag">Tags:</label>
|
||||
<select id="filter-tag" multiple aria-label="Filter by tags"></select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
|
||||
<div id="samples-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading samples...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
padding: 64px 32px;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
<Modal />
|
||||
|
||||
<style is:global>
|
||||
/* Cookbook Section */
|
||||
.cookbook-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.coming-soon-icon {
|
||||
font-size: 64px;
|
||||
.cookbook-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.coming-soon h2 {
|
||||
color: var(--color-text-emphasis);
|
||||
margin-bottom: 16px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.coming-soon p {
|
||||
color: var(--color-text-muted);
|
||||
max-width: 500px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.sample-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 24px auto;
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sample-list li {
|
||||
padding: 8px 0;
|
||||
color: var(--color-text);
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.sample-list li::before {
|
||||
content: '→ ';
|
||||
color: var(--color-link);
|
||||
.cookbook-info h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.coming-soon-actions {
|
||||
margin-top: 32px;
|
||||
.cookbook-info p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cookbook-languages {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lang-tab {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.lang-tab:hover {
|
||||
border-color: var(--color-link);
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.lang-tab.active {
|
||||
border-color: var(--color-link);
|
||||
background: var(--color-link);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Recipe Grid */
|
||||
.recipes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Recipe Card */
|
||||
.recipe-card {
|
||||
background: var(--color-card-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 24px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.recipe-card:hover {
|
||||
border-color: var(--color-link);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.recipe-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.recipe-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.recipe-langs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lang-indicator {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.recipe-description {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.recipe-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.recipe-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.recipe-actions .btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recipe-actions .btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-variant {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64px 24px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Search highlight */
|
||||
.search-highlight {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-text-emphasis);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.cookbook-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recipe-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipe-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/samples';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
|
||||
477
website/src/scripts/pages/samples.ts
Normal file
477
website/src/scripts/pages/samples.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
/**
|
||||
* Samples/Cookbook page functionality
|
||||
*/
|
||||
|
||||
import { FuzzySearch, type SearchableItem } from '../search';
|
||||
import { fetchData, fetchFileContent, escapeHtml } from '../utils';
|
||||
|
||||
// Types
|
||||
interface Language {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
interface RecipeVariant {
|
||||
doc: string;
|
||||
example: string | null;
|
||||
}
|
||||
|
||||
interface Recipe extends SearchableItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
variants: Record<string, RecipeVariant>;
|
||||
}
|
||||
|
||||
interface Cookbook {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
path: string;
|
||||
featured: boolean;
|
||||
languages: Language[];
|
||||
recipes: Recipe[];
|
||||
}
|
||||
|
||||
interface SamplesData {
|
||||
cookbooks: Cookbook[];
|
||||
totalRecipes: number;
|
||||
totalCookbooks: number;
|
||||
filters: {
|
||||
languages: string[];
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// State
|
||||
let samplesData: SamplesData | null = null;
|
||||
let search: FuzzySearch<Recipe & { title: string; cookbookId: string }> | null = null;
|
||||
let selectedLanguage: string | null = null;
|
||||
let selectedTags: string[] = [];
|
||||
let expandedRecipes: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize search with all recipes
|
||||
const allRecipes = samplesData.cookbooks.flatMap(cookbook =>
|
||||
cookbook.recipes.map(recipe => ({
|
||||
...recipe,
|
||||
title: recipe.name,
|
||||
cookbookId: cookbook.id
|
||||
}))
|
||||
);
|
||||
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');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No Samples Available</h3>
|
||||
<p>Check back soon for code samples and recipes.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Hide filters
|
||||
const filtersBar = document.getElementById('filters-bar');
|
||||
if (filtersBar) filtersBar.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup language and tag filters
|
||||
*/
|
||||
function setupFilters(): void {
|
||||
if (!samplesData) return;
|
||||
|
||||
// Language filter
|
||||
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 => {
|
||||
if (!languages.has(lang.id)) {
|
||||
languages.set(lang.id, lang);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
languageSelect.innerHTML = '<option value="">All Languages</option>';
|
||||
languages.forEach((lang, id) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = id;
|
||||
option.textContent = `${lang.icon} ${lang.name}`;
|
||||
languageSelect.appendChild(option);
|
||||
});
|
||||
|
||||
languageSelect.addEventListener('change', () => {
|
||||
selectedLanguage = languageSelect.value || null;
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Tag filter (multi-select with Choices.js if available)
|
||||
const tagSelect = document.getElementById('filter-tag') as HTMLSelectElement;
|
||||
if (tagSelect && samplesData.filters.tags.length > 0) {
|
||||
tagSelect.innerHTML = '';
|
||||
samplesData.filters.tags.forEach(tag => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tag;
|
||||
option.textContent = tag;
|
||||
tagSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Initialize Choices.js if available
|
||||
if (typeof window !== 'undefined' && (window as any).Choices) {
|
||||
const choices = new (window as any).Choices(tagSelect, {
|
||||
removeItemButton: true,
|
||||
placeholder: true,
|
||||
placeholderValue: 'Filter by tags...',
|
||||
searchPlaceholderValue: 'Search tags...',
|
||||
noResultsText: 'No tags found',
|
||||
noChoicesText: 'No tags available',
|
||||
});
|
||||
|
||||
tagSelect.addEventListener('change', () => {
|
||||
selectedTags = choices.getValue(true) as string[];
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
});
|
||||
} else {
|
||||
tagSelect.multiple = true;
|
||||
tagSelect.addEventListener('change', () => {
|
||||
selectedTags = Array.from(tagSelect.selectedOptions).map(o => o.value);
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear filters button
|
||||
const clearBtn = document.getElementById('clear-filters');
|
||||
clearBtn?.addEventListener('click', clearFilters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup search functionality
|
||||
*/
|
||||
function setupSearch(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
if (!searchInput) return;
|
||||
|
||||
let debounceTimer: number;
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = window.setTimeout(() => {
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
function clearFilters(): void {
|
||||
selectedLanguage = null;
|
||||
selectedTags = [];
|
||||
|
||||
const languageSelect = document.getElementById('filter-language') as HTMLSelectElement;
|
||||
if (languageSelect) languageSelect.value = '';
|
||||
|
||||
const tagSelect = document.getElementById('filter-tag') as HTMLSelectElement;
|
||||
if (tagSelect && (window as any).Choices) {
|
||||
// Clear Choices.js selection
|
||||
const choicesInstance = tagSelect.closest('.choices');
|
||||
if (choicesInstance) {
|
||||
const choices = (tagSelect as any).choices;
|
||||
if (choices) choices.removeActiveItems();
|
||||
}
|
||||
} else if (tagSelect) {
|
||||
Array.from(tagSelect.options).forEach(o => o.selected = false);
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
if (searchInput) searchInput.value = '';
|
||||
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered recipes
|
||||
*/
|
||||
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() || '';
|
||||
|
||||
let results: { cookbook: Cookbook; recipe: Recipe; highlighted?: string }[] = [];
|
||||
|
||||
if (query) {
|
||||
// Use fuzzy search
|
||||
const searchResults = search.search(query);
|
||||
results = searchResults.map(result => {
|
||||
const cookbook = samplesData!.cookbooks.find(c => c.id === result.item.cookbookId)!;
|
||||
return {
|
||||
cookbook,
|
||||
recipe: result.item,
|
||||
highlighted: search!.highlight(result.item.title, query)
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// No search query - return all recipes
|
||||
results = samplesData.cookbooks.flatMap(cookbook =>
|
||||
cookbook.recipes.map(recipe => ({ cookbook, recipe }))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply language filter
|
||||
if (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))
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cookbooks and recipes
|
||||
*/
|
||||
function renderCookbooks(): void {
|
||||
const container = document.getElementById('samples-list');
|
||||
if (!container || !samplesData) return;
|
||||
|
||||
const filteredResults = getFilteredRecipes();
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No Results Found</h3>
|
||||
<p>Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by cookbook
|
||||
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: [] });
|
||||
}
|
||||
byCookbook.get(cookbook.id)!.recipes.push({ recipe, highlighted });
|
||||
});
|
||||
|
||||
let html = '';
|
||||
byCookbook.forEach(({ cookbook, recipes }) => {
|
||||
html += renderCookbookSection(cookbook, recipes);
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Setup event listeners
|
||||
setupRecipeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}"
|
||||
title="${lang.name}">
|
||||
${lang.icon}
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
const recipeCards = recipes.map(({ recipe, highlighted }) =>
|
||||
renderRecipeCard(cookbook, recipe, highlighted)
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="cookbook-section" data-cookbook="${cookbook.id}">
|
||||
<div class="cookbook-header">
|
||||
<div class="cookbook-info">
|
||||
<h2>${escapeHtml(cookbook.name)}</h2>
|
||||
<p>${escapeHtml(cookbook.description)}</p>
|
||||
</div>
|
||||
<div class="cookbook-languages">
|
||||
${languageTabs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recipes-grid">
|
||||
${recipeCards}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a recipe card
|
||||
*/
|
||||
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 variant = recipe.variants[displayLang];
|
||||
|
||||
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('');
|
||||
|
||||
return `
|
||||
<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>
|
||||
</div>
|
||||
<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}">
|
||||
<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 ? `
|
||||
<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}"
|
||||
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>'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for recipe interactions
|
||||
*/
|
||||
function setupRecipeListeners(): void {
|
||||
// View recipe buttons
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// View example buttons
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Language tab clicks
|
||||
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;
|
||||
if (languageSelect) languageSelect.value = langId;
|
||||
renderCookbooks();
|
||||
updateResultsCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show recipe/example content in modal
|
||||
*/
|
||||
async function showRecipeContent(filePath: string, type: 'recipe' | 'example'): Promise<void> {
|
||||
// Use existing modal infrastructure
|
||||
const { openFileModal } = await import('../modal');
|
||||
await openFileModal(filePath, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update results count display
|
||||
*/
|
||||
function updateResultsCount(): void {
|
||||
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' : ''}`;
|
||||
} else {
|
||||
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());
|
||||
} else {
|
||||
initSamplesPage();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user