/** Default placeholder image for missing thumbnails (base64 encoded) */ const FALLBACK_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iIzJBMkEyQSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCI+Tm8gUHJldmlldzwvdGV4dD48L3N2Zz4='; /** Animation delay for card transitions in milliseconds */ const FILTER_TRANSITION_DELAY = 300; /** Number of columns for masonry grid */ const GRID_COLUMN_COUNT = 4; /** Duration for copy feedback notification in milliseconds */ const COPY_NOTIFICATION_DURATION = 1000; /** URL separator for categories (pipe doesn't get encoded) */ const URL_CATEGORY_SEPARATOR = '|'; document.addEventListener('DOMContentLoaded', async () => { const grid = document.querySelector('[data-material-grid]'); const searchInput = document.querySelector('[data-search]'); const categoriesContainer = document.querySelector('[data-categories]'); const loadingSpinner = document.querySelector('[data-loading]'); const clearFiltersButton = document.querySelector('[data-clear-filters]'); const resultCountEl = document.querySelector('[data-result-count]'); let materialLibrary = []; /** * Shows or hides the loading spinner * @param {boolean} show - Whether to show the spinner */ function setLoading(show) { loadingSpinner.style.display = show ? 'flex' : 'none'; grid.style.display = show ? 'none' : 'block'; } /** * Updates the result count display * @param {number} count - Number of materials shown * @param {number} total - Total number of materials */ function updateResultCount(count, total) { if (count === total) { resultCountEl.innerHTML = `Showing ${count} snippets`; } else { resultCountEl.innerHTML = `Showing ${count} of ${total} snippets`; } } /** * Reads URL parameters and returns filter state * @returns {{search: string, categories: string[]}} Filter state from URL */ function getFiltersFromURL() { const params = new URLSearchParams(window.location.search); const search = params.get('q') || ''; const categories = params.get('categories') ? params.get('categories').split(URL_CATEGORY_SEPARATOR) : []; return { search, categories }; } /** * Updates the URL with current filter state * @param {string} search - Current search term * @param {string[]} categories - Currently selected categories */ function updateURL(search, categories) { const params = new URLSearchParams(); if (search) params.set('q', search); if (categories.length > 0) params.set('categories', categories.join(URL_CATEGORY_SEPARATOR)); const newURL = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; window.history.replaceState({}, '', newURL); } /** * Updates the visibility of the clear filters button */ function updateClearFiltersVisibility() { const hasActiveFilters = document.querySelectorAll('.category-button.active').length > 0; const hasSearchText = searchInput.value.trim().length > 0; clearFiltersButton.style.display = (hasActiveFilters || hasSearchText) ? 'inline-block' : 'none'; } /** * Clears all active filters and search text */ function clearAllFilters() { document.querySelectorAll('.category-button.active').forEach(btn => btn.classList.remove('active')); searchInput.value = ''; updateClearFiltersVisibility(); filterMaterials(); } /** * Shows a copy notification at the given position * @param {DOMRect} rect - Bounding rectangle for positioning */ function showCopyNotification(rect) { const notification = document.createElement('div'); notification.className = 'copy-notification'; notification.textContent = 'Copied!'; notification.style.left = `${rect.left + rect.width / 2}px`; notification.style.top = `${rect.top}px`; document.body.appendChild(notification); setTimeout(() => notification.remove(), COPY_NOTIFICATION_DURATION); } setLoading(true); try { const response = await fetch('materials.json'); const data = await response.json(); materialLibrary = data.materials; const categories = [...new Set(materialLibrary.flatMap(m => m.categories))].sort(); categoriesContainer.innerHTML = categories.map(category => ` `).join(''); const categoryButtons = categoriesContainer.querySelectorAll('.category-button'); categoryButtons.forEach(button => { button.addEventListener('click', () => { button.classList.toggle('active'); updateClearFiltersVisibility(); filterMaterials(); }); }); clearFiltersButton.addEventListener('click', clearAllFilters); const urlFilters = getFiltersFromURL(); if (urlFilters.search) { searchInput.value = urlFilters.search; } if (urlFilters.categories.length > 0) { urlFilters.categories.forEach(cat => { const btn = categoriesContainer.querySelector(`[data-category="${cat}"]`); if (btn) btn.classList.add('active'); }); } updateClearFiltersVisibility(); setLoading(false); } catch (error) { console.error('Error loading materials:', error); setLoading(false); grid.innerHTML = '
'; return; } /** * Renders the material cards to the grid * @param {Array} materials - Array of material objects to render */ function renderMaterials(materials) { grid.innerHTML = materials.map(material => `${material.description}