@quasarbright/projection
Version:
A static site generator that creates a beautiful, interactive gallery to showcase your coding projects. Features search, filtering, tags, responsive design, and an admin UI.
261 lines (213 loc) • 7.48 kB
JavaScript
// Filter and sort functionality
class ProjectFilter {
constructor() {
this.tagFilters = document.querySelectorAll('.tag-filter');
this.projectTags = document.querySelectorAll('.project-tags .tag');
this.sortSelect = document.getElementById('sort-select');
this.clearFiltersBtn = document.getElementById('clear-filters');
this.projectsGrid = document.getElementById('projects-grid');
this.activeFilters = new Set(['all']);
this.currentSort = 'date';
this.searchActive = false;
this.init();
}
init() {
// Tag filter buttons
this.tagFilters.forEach(filter => {
filter.addEventListener('click', (e) => {
this.toggleTagFilter(e.target.dataset.tag);
});
});
// Tags in project cards (for quick filtering)
this.projectTags.forEach(tag => {
tag.addEventListener('click', (e) => {
e.preventDefault();
this.toggleTagFilter(e.target.dataset.tag);
});
});
// Sort select
this.sortSelect.addEventListener('change', (e) => {
this.setSortOrder(e.target.value);
});
// Clear filters button
this.clearFiltersBtn.addEventListener('click', () => {
this.clearAllFilters();
});
}
toggleTagFilter(tag) {
if (tag === 'all') {
this.clearAllFilters();
return;
}
// Remove 'all' filter when selecting specific tags
this.activeFilters.delete('all');
if (this.activeFilters.has(tag)) {
this.activeFilters.delete(tag);
} else {
this.activeFilters.add(tag);
}
// If no filters active, activate 'all'
if (this.activeFilters.size === 0) {
this.activeFilters.add('all');
}
this.updateFilterUI();
this.applyFilters();
}
updateFilterUI() {
this.tagFilters.forEach(filter => {
const tag = filter.dataset.tag;
filter.classList.toggle('active', this.activeFilters.has(tag));
});
// Update tag highlighting in cards and modal
this.updateTagHighlighting();
}
updateTagHighlighting() {
// Update tags in project cards
document.querySelectorAll('.project-tags .tag').forEach(tag => {
const tagName = tag.dataset.tag;
tag.classList.toggle('active-filter', this.activeFilters.has(tagName) && !this.activeFilters.has('all'));
});
// Update tags in modal if it's open
document.querySelectorAll('.modal-tags .tag').forEach(tag => {
const tagName = tag.dataset.tag;
tag.classList.toggle('active-filter', this.activeFilters.has(tagName) && !this.activeFilters.has('all'));
});
}
applyFilters() {
// If search is active, don't override search results
if (this.searchActive) {
return;
}
const projectCards = document.querySelectorAll('.project-card');
let visibleCount = 0;
projectCards.forEach(card => {
if (this.shouldShowProject(card)) {
card.style.display = 'block';
card.classList.remove('filtered-out');
visibleCount++;
} else {
card.style.display = 'none';
card.classList.add('filtered-out');
}
});
// Apply sorting to visible cards
this.sortProjects();
// Show/hide no results message
this.toggleNoResults(visibleCount === 0);
// Update URL with active filters
this.updateURL();
}
shouldShowProject(card) {
// If 'all' is active, show all projects
if (this.activeFilters.has('all')) {
return true;
}
const projectId = card.dataset.projectId;
const project = window.PROJECTS_DATA.projects.find(p => p.id === projectId);
if (!project) return false;
// Check if project has ALL of the active filter tags
return [...this.activeFilters].every(filterTag => {
// Handle 'featured' specially - check the featured field instead of tags
if (filterTag === 'featured') {
return project.featured === true;
}
// For all other tags, check the tags array
return project.tags.includes(filterTag);
});
}
setSortOrder(sortOrder) {
this.currentSort = sortOrder;
this.sortProjects();
this.updateURL();
}
sortProjects() {
const projectsContainer = this.projectsGrid;
const projectCards = Array.from(projectsContainer.querySelectorAll('.project-card'));
// Only sort visible cards
const visibleCards = projectCards.filter(card =>
card.style.display !== 'none'
);
visibleCards.sort((a, b) => {
const projectA = window.PROJECTS_DATA.projects.find(p => p.id === a.dataset.projectId);
const projectB = window.PROJECTS_DATA.projects.find(p => p.id === b.dataset.projectId);
switch (this.currentSort) {
case 'date':
return new Date(projectB.creationDate) - new Date(projectA.creationDate);
case 'date-asc':
return new Date(projectA.creationDate) - new Date(projectB.creationDate);
case 'name':
return projectA.title.localeCompare(projectB.title);
case 'name-desc':
return projectB.title.localeCompare(projectA.title);
default:
return new Date(projectB.creationDate) - new Date(projectA.creationDate);
}
});
// Reorder DOM elements
visibleCards.forEach(card => {
projectsContainer.appendChild(card);
});
}
clearAllFilters() {
this.activeFilters.clear();
this.activeFilters.add('all');
this.updateFilterUI();
this.applyFilters();
// Clear search as well
if (window.projectSearch) {
window.projectSearch.clearSearch();
}
}
toggleNoResults(show) {
const noResults = document.getElementById('no-results');
noResults.style.display = show ? 'block' : 'none';
}
updateURL() {
const params = {};
// Add active filters to URL (except 'all')
const filterTags = [...this.activeFilters].filter(tag => tag !== 'all');
if (filterTags.length > 0) {
params.tags = filterTags.join(',');
}
// Add sort order to URL
params.sort = this.currentSort;
const url = new URL(window.location);
// Remove all our parameters first
url.searchParams.delete('tags');
url.searchParams.delete('sort');
// Then add the current ones
Object.entries(params).forEach(([key, value]) => {
if (value) {
url.searchParams.set(key, value);
}
});
window.history.replaceState({}, '', url);
}
// Public method to set filters from external sources (like URL params)
setFilters(tags, sort) {
if (tags && tags.length > 0) {
this.activeFilters.clear();
tags.forEach(tag => this.activeFilters.add(tag));
}
if (sort) {
this.currentSort = sort;
this.sortSelect.value = sort;
}
this.updateFilterUI();
this.applyFilters();
}
}
// Initialize filters when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.projectFilter = new ProjectFilter();
// Load filters from URL parameters if present
const urlParams = new URLSearchParams(window.location.search);
const tagFilters = urlParams.get('tags');
const sortOrder = urlParams.get('sort');
if (tagFilters || sortOrder) {
const tags = tagFilters ? tagFilters.split(',') : [];
window.projectFilter.setFilters(tags, sortOrder);
}
// Initial tag highlighting
window.projectFilter.updateTagHighlighting();
});