diff --git a/docs/script.js b/docs/script.js index ce656eb..d5727a3 100644 --- a/docs/script.js +++ b/docs/script.js @@ -32,6 +32,13 @@ let allUsers = []; let filteredUsers = []; let isDataLoaded = false; +// ============================================================================ +// CONSTANTS +// ============================================================================ +const MAX_FOLLOWERS = 999999999; +const MAX_REPOS = 999999; +const MAX_FORKS = 999999; + // ============================================================================ // INITIALIZATION // ============================================================================ @@ -379,6 +386,11 @@ function setupEventListeners() { randomBtn.type = 'button'; randomBtn.addEventListener('click', pickRandomUser); } + + const clearFiltersBtn = document.querySelector('.clear-filters-btn'); + if (clearFiltersBtn) { + clearFiltersBtn.addEventListener('click', resetFilters); + } } /** @@ -525,16 +537,16 @@ function getActiveFilters() { */ function validateRangeFilters(filters) { if (filters.minFollowers > filters.maxFollowers) { - document.getElementById('maxFollowersFilter').value = '999999999'; - filters.maxFollowers = 999999999; + document.getElementById('maxFollowersFilter').value = String(MAX_FOLLOWERS); + filters.maxFollowers = MAX_FOLLOWERS; } if (filters.minRepos > filters.maxRepos) { - document.getElementById('maxReposFilter').value = '999999'; - filters.maxRepos = 999999; + document.getElementById('maxReposFilter').value = String(MAX_REPOS); + filters.maxRepos = MAX_REPOS; } if (filters.minForks > filters.maxForks) { - document.getElementById('maxForksFilter').value = '999999'; - filters.maxForks = 999999; + document.getElementById('maxForksFilter').value = String(MAX_FORKS); + filters.maxForks = MAX_FORKS; } } @@ -743,6 +755,7 @@ function updateVisibilityAndSort() { renderCards(sortedUsers); updateCounts(sortedUsers); updateResultsMessage(sortedUsers); + updateActiveFiltersIndicator(); } /** @@ -1030,6 +1043,174 @@ function updateResultsMessage(sortedUsers) { } } +// ============================================================================ +// ACTIVE FILTERS INDICATOR +// ============================================================================ +/** + * Update the active filters indicator display + */ +function updateActiveFiltersIndicator() { + const indicator = document.getElementById('activeFiltersIndicator'); + const tagsContainer = document.getElementById('activeFiltersTags'); + if (!indicator || !tagsContainer) return; + + const filters = getActiveFilters(); + const sortBy = document.getElementById('sortBy').value; + const activeTags = []; + + const sortOption = document.querySelector(`#sortBy option[value="${sortBy}"]`); + if (sortOption && sortBy !== 'followers-desc') { + activeTags.push({ + label: `Sort: ${sortOption.textContent.trim()}`, + type: 'sort', + value: sortBy, + }); + } + + if (filters.searchTerm) { + activeTags.push({ + label: `Search: "${filters.searchTerm}"`, + type: 'search', + value: filters.searchTerm, + }); + } + + addRangeFilterTag(activeTags, filters, { + minKey: 'minFollowers', + maxKey: 'maxFollowers', + maxValue: MAX_FOLLOWERS, + labelPrefix: 'Followers', + tagType: 'followers', + useFormat: true, + }); + + addRangeFilterTag(activeTags, filters, { + minKey: 'minRepos', + maxKey: 'maxRepos', + maxValue: MAX_REPOS, + labelPrefix: 'Repos', + tagType: 'repos', + useFormat: true, + }); + + addRangeFilterTag(activeTags, filters, { + minKey: 'minForks', + maxKey: 'maxForks', + maxValue: MAX_FORKS, + labelPrefix: 'Forks', + tagType: 'forks', + useFormat: true, + }); + if (filters.minStars > 0) { + activeTags.push({ + label: `Stars: ${formatNumber(filters.minStars)}+`, + type: 'stars', + }); + } + const selectFilters = [ + { key: 'sponsorsFilter', label: 'Sponsors', type: 'sponsors' }, + { key: 'sponsoringFilter', label: 'Sponsoring', type: 'sponsoring' }, + { key: 'avatarAgeFilter', label: 'Avatar', type: 'avatar' }, + { key: 'lastRepoActivityFilter', label: 'Repo Activity', type: 'repo-activity' }, + { key: 'lastCommitFilter', label: 'Last Commit', type: 'commit' }, + ]; + + selectFilters.forEach((filter) => { + addSelectFilterTag(activeTags, filters, filter.key, filter.key, filter.label, filter.type); + }); + + if (filters.languageFilter) { + activeTags.push({ + label: `Language: ${filters.languageFilter}`, + type: 'language', + }); + } + // Clear existing tags + tagsContainer.replaceChildren(); + // Show/hide indicator based on active filters + if (activeTags.length > 0) { + indicator.style.display = 'block'; + const fragment = document.createDocumentFragment(); + activeTags.forEach((tag) => { + const tagElement = document.createElement('span'); + tagElement.className = 'filter-tag'; + tagElement.setAttribute('data-filter-type', tag.type); + tagElement.textContent = tag.label; + fragment.appendChild(tagElement); + }); + tagsContainer.appendChild(fragment); + } else { + indicator.style.display = 'none'; + } +} +/** + * Format number with K, M suffixes for display + * @param {number} num - Number to format + * @returns {string} Formatted number string + */ +function formatNumber(num) { + if (num >= 1000000) { + return `${(num / 1000000).toFixed(1)}M`; + } + if (num >= 1000) { + return `${(num / 1000).toFixed(1)}K`; + } + return num.toString(); +} + +/** + * Add a range filter tag to activeTags if the filter is active + * @param {Array} activeTags - Array to add the tag to + * @param {Object} filters - Filters object + * @param {Object} options - Configuration object + * @param {string} options.minKey - Key for minimum value in filters object + * @param {string} options.maxKey - Key for maximum value in filters object + * @param {number} options.maxValue - Maximum value constant (e.g., MAX_FOLLOWERS) + * @param {string} options.labelPrefix - Prefix for the tag label (e.g., "Followers") + * @param {string} options.tagType - Type identifier for the tag + * @param {boolean} options.useFormat - Whether to use formatNumber for display (default: false) + */ +function addRangeFilterTag(activeTags, filters, { minKey, maxKey, maxValue, labelPrefix, tagType, useFormat = false }) { + const min = filters[minKey]; + const max = filters[maxKey]; + + if (min > 0 || max < maxValue) { + let label = `${labelPrefix}: `; + const format = (num) => useFormat ? formatNumber(num) : num; + + if (min > 0 && max < maxValue) { + label += `${format(min)} - ${format(max)}`; + } else if (min > 0) { + label += `${format(min)}+`; + } else { + label += `≤ ${format(max)}`; + } + activeTags.push({ label, type: tagType }); + } +} + +/** + * Add a select-based filter tag to activeTags if the filter is active + * @param {Array} activeTags - Array to add the tag to + * @param {Object} filters - Filters object + * @param {string} filterKey - Key in filters object + * @param {string} elementId - ID of the select element + * @param {string} labelPrefix - Prefix for the tag label (e.g., "Sponsors") + * @param {string} tagType - Type identifier for the tag + */ +function addSelectFilterTag(activeTags, filters, filterKey, elementId, labelPrefix, tagType) { + if (filters[filterKey] !== 'any') { + const select = document.getElementById(elementId); + const option = select?.options[select.selectedIndex]; + if (option) { + activeTags.push({ + label: `${labelPrefix}: ${option.textContent.trim()}`, + type: tagType, + }); + } + } +} + // ============================================================================ // RESET FILTERS // ============================================================================ @@ -1041,11 +1222,11 @@ function resetFilters() { searchInput: '', sortBy: 'followers-desc', followersFilter: '0', - maxFollowersFilter: '999999999', + maxFollowersFilter: String(MAX_FOLLOWERS), minReposFilter: '0', - maxReposFilter: '999999', + maxReposFilter: String(MAX_REPOS), minForksFilter: '0', - maxForksFilter: '999999', + maxForksFilter: String(MAX_FORKS), sponsorsFilter: 'any', sponsoringFilter: 'any', avatarAgeFilter: 'any', diff --git a/docs/styles.css b/docs/styles.css index 67148f6..2f6f373 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -380,6 +380,126 @@ h1 { padding-right: 20px; } +/* ============================================================================ */ +/* ACTIVE FILTERS INDICATOR */ +/* ============================================================================ */ +.active-filters-indicator { + display: none; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border: 2px solid #667eea; + border-radius: 12px; + padding: 16px 20px; + margin: 20px auto; + max-width: 1400px; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); +} + +.active-filters-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + flex-wrap: wrap; + gap: 10px; +} + +.active-filters-title { + font-weight: 600; + font-size: 14px; + color: #333; + display: flex; + align-items: center; + gap: 6px; +} + +.active-filters-title::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-size: contain; + background-position: center; + vertical-align: middle; +} + +.clear-filters-btn { + padding: 6px 14px; + background: rgba(255, 100, 100, 0.1); + border: 1px solid rgba(255, 100, 100, 0.3); + color: #d32f2f; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.2s; +} + +.clear-filters-btn:hover { + background: rgba(255, 100, 100, 0.2); + border-color: rgba(255, 100, 100, 0.5); + transform: translateY(-1px); +} + +.active-filters-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-tag { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3); + transition: all 0.2s; +} + +.filter-tag:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4); +} + +.filter-tag[data-filter-type='sort'] { + background: linear-gradient(135deg, #ff9f43 0%, #ff6b6b 100%); +} + +.filter-tag[data-filter-type='search'] { + background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); +} + +@media (max-width: 768px) { + .active-filters-indicator { + margin: 15px 10px; + padding: 12px 16px; + } + + .active-filters-header { + flex-direction: column; + align-items: flex-start; + } + + .clear-filters-btn { + width: 100%; + padding: 8px; + } + + .active-filters-tags { + gap: 6px; + } + + .filter-tag { + font-size: 11px; + padding: 5px 10px; + } +} + /* ============================================================================ */ /* FEATURED USER SECTION */ /* ============================================================================ */ diff --git a/layouts/layout.html b/layouts/layout.html index c3ad778..f84f91a 100644 --- a/layouts/layout.html +++ b/layouts/layout.html @@ -458,6 +458,14 @@
+