Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 190 additions & 9 deletions docs/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -743,6 +755,7 @@ function updateVisibilityAndSort() {
renderCards(sortedUsers);
updateCounts(sortedUsers);
updateResultsMessage(sortedUsers);
updateActiveFiltersIndicator();
}

/**
Expand Down Expand Up @@ -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
// ============================================================================
Expand All @@ -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',
Expand Down
120 changes: 120 additions & 0 deletions docs/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
/* ============================================================================ */
Expand Down
8 changes: 8 additions & 0 deletions layouts/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,14 @@ <h2 id="featuredUserName" class="featured-name"></h2>
</div>
</div>

<div class="active-filters-indicator" id="activeFiltersIndicator">
<div class="active-filters-header">
<span class="active-filters-title">Active Filters & Sort:</span>
<button class="clear-filters-btn" aria-label="Clear all filters">Clear All</button>
</div>
<div class="active-filters-tags" id="activeFiltersTags"></div>
</div>

<div class="grid" id="grid"></div>
</main>
</div>
Expand Down