-
${attributes.name}
- ${attributes.bookmarked ? '' : ''}
-
-
@${attributes.handle}
+ programs.forEach(program => {
+ const { attributes } = program;
+ template.innerHTML = `
+
+ `;
+ const cardNode = template.content.firstElementChild.cloneNode(true);
+ cardNode.querySelector('.card-body').innerHTML = generateCardContent(attributes);
+ fragment.appendChild(cardNode);
+ });
-
- ${attributes.submission_state === 'open' ? 'Open for Submission' : 'Closed'}
- ${attributes.state === 'public_mode' ? 'Public Program' : 'Private Program'}
- ${attributes.offers_bounties ? 'Bounty $$$' : 'VDP'}
- ${attributes.open_scope ? 'Open Scope' : ''}
- ${isProgramNew(attributes.started_accepting_at) ? ' New' : ''}
-
+ container.appendChild(fragment);
+ initializeFilter();
+ }
-
-
My Reports: ${attributes.number_of_reports_for_user}
-
My Earnings: $${attributes.bounty_earned_for_user.toFixed(2)}
-
-
-
-
-
-
Since ${new Date(attributes.started_accepting_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
-
${attributes.currency.toUpperCase()}
-
-
See details
+ function generateCardContent(attributes) {
+ return `
+
+

+
- `;
-
-
- // Initialize tooltips esp for bookmarked programs
- const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
- tooltips.forEach((tooltip) => {
- new bootstrap.Tooltip(tooltip);
- });
- container.appendChild(card);
-
- initializeFilter();
-
- });
+
+ ${attributes.submission_state === 'open' ? 'Open' : 'Closed'}
+ ${attributes.state === 'public_mode' ? 'Public' : 'Private'}
+ ${attributes.offers_bounties ? 'Bounty' : 'VDP'}
+ ${attributes.open_scope ? 'Open Scope' : ''}
+ ${isProgramNew(attributes.started_accepting_at) ? ' New' : ''}
+
+
+
Reports: ${attributes.number_of_reports_for_user}
+
Earnings: $${attributes.bounty_earned_for_user.toFixed(2)}
+
+
+
+
Since ${new Date(attributes.started_accepting_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
+
${attributes.currency.toUpperCase()}
+
+
+ `;
}
function isProgramNew(startedAcceptingAt) {
const threeMonthsAgo = new Date();
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
- const startedAcceptingDate = new Date(startedAcceptingAt);
- return startedAcceptingDate > threeMonthsAgo;
+ return new Date(startedAcceptingAt) > threeMonthsAgo;
}
- function displayErrorMessage(message) {
- const container = document.getElementById('program_cards');
- container.innerHTML = `
-
- `;
- }
-
- // below has everything to do with card selection and import button
- const container = document.getElementById('program_cards');
- const importBtn = document.getElementById('importProgramsBtn');
- const clearBtn = document.getElementById('clearSelectionsLink');
-
- container.addEventListener('click', function(event) {
- const card = event.target.closest('.card-selectable');
- if (card) {
- toggleCardSelection(event, card);
- }
- });
-
- function toggleCardSelection(event, card) {
- if (event.target.closest('#btn-see-details') || event.target.closest('#handle-link')) {
- // If it's the "See details" button, don't toggle selection, maybe we need other actions in the future here
- return;
- }
-
- card.classList.toggle('card-selected');
- updateImportButton();
- }
-
- function updateImportButton() {
- const selectedCards = container.querySelectorAll('.card-selected');
- const count = selectedCards.length;
-
- if (count === 0) {
- importBtn.disabled = true;
- importBtn.innerHTML = '
Import Programs';
- clearBtn.style.display = 'none';
- } else {
- importBtn.disabled = false;
- importBtn.innerHTML = `
Import ${count} Program${count !== 1 ? 's' : ''}`;
- clearBtn.style.display = 'inline';
- }
- }
-
- function clearAllSelections() {
- const selectedCards = container.querySelectorAll('.card-selected');
- selectedCards.forEach(card => card.classList.remove('card-selected'));
- updateImportButton();
- }
-
- // clear btn listener
- clearBtn.addEventListener('click', function(event) {
- event.preventDefault();
- clearAllSelections();
- });
-
-
- // init state
- updateImportButton();
-
-
- // we begin filtering here
function initializeFilter() {
- const filterSelect = document.querySelector('select[aria-label="Program type"]');
- const container = document.getElementById('program_cards');
const allCards = Array.from(container.querySelectorAll('.program-card-wrapper'));
- const searchInput = document.querySelector('#search-program-box');
- const showClosedCheckbox = document.getElementById('show-closed-programs');
-
- // Pre-compute card data to avoid querying the DOM on each filter/search
- const cardData = allCards.map(cardWrapper => {
- const card = cardWrapper.querySelector('.bbp-card');
- return {
- wrapper: cardWrapper,
- name: card.querySelector('h5').textContent.toLowerCase(),
- offersBounties: card.dataset.offersBounties === 'true',
- isPrivate: card.dataset.programState === 'private_mode',
- isClosed: card.querySelector('.badge').textContent.trim() !== 'Open for Submission'
- };
- });
-
- let lastFilter = '';
- let lastSearch = '';
-
- function filterAndSearchCards() {
+ const cardData = allCards.map(createCardData);
+
+ const filterAndSearchCards = debounce(() => {
const selectedFilter = filterSelect.value;
const searchTerm = searchInput.value.toLowerCase().trim();
const showClosed = showClosedCheckbox.checked;
-
- if (selectedFilter === lastFilter && searchTerm === lastSearch && showClosed === lastShowClosed) return;
- lastFilter = selectedFilter;
- lastSearch = searchTerm;
- lastShowClosed = showClosed;
-
+
const visibleCards = cardData.filter(({ offersBounties, isPrivate, name, isClosed }) => {
let shouldShow = true;
-
+
switch(selectedFilter) {
case 'Bounty Eligible':
shouldShow = offersBounties;
@@ -248,71 +147,99 @@ document.addEventListener('DOMContentLoaded', function() {
shouldShow = isPrivate;
break;
}
-
- shouldShow = shouldShow && (!searchTerm || name.includes(searchTerm));
+ shouldShow = shouldShow && (!searchTerm || name.includes(searchTerm));
shouldShow = shouldShow && (showClosed || !isClosed);
return shouldShow;
});
-
- // Batch DOM updates to avoid reflows and make the transition smoother
+
requestAnimationFrame(() => {
- cardData.forEach(({ wrapper }) => {
- wrapper.style.display = 'none';
- wrapper.classList.add('filtering-hide');
- });
-
- visibleCards.forEach(({ wrapper }) => {
- wrapper.style.display = '';
- wrapper.classList.remove('filtering-hide');
- });
+ cardData.forEach(({ wrapper }) => wrapper.style.display = 'none');
+ visibleCards.forEach(({ wrapper }) => wrapper.style.display = '');
});
- }
-
+ }, 100);
+
filterSelect.addEventListener('change', filterAndSearchCards);
searchInput.addEventListener('input', filterAndSearchCards);
showClosedCheckbox.addEventListener('change', filterAndSearchCards);
- filterAndSearchCards(); // init call
+ filterAndSearchCards();
+ }
+
+ function createCardData(cardWrapper) {
+ const card = cardWrapper.querySelector('.bbp-card');
+ return {
+ wrapper: cardWrapper,
+ name: card.querySelector('h5').textContent.toLowerCase(),
+ offersBounties: card.dataset.offersBounties === 'true',
+ isPrivate: card.dataset.programState === 'private_mode',
+ isClosed: card.querySelector('.badge').textContent.trim() !== 'Open'
+ };
}
- // in this function, we handle the sort select
function updateSortingParams() {
- const sortSelect = document.getElementById('sort-select');
- let sortBy, sortOrder;
-
- if (sortSelect.value === 'Sort by' || !sortSelect.value) {
- sortBy = 'age';
- sortOrder = 'desc';
- } else {
- [sortBy, sortOrder] = sortSelect.value.split('-');
- }
+ const [sortBy, sortOrder] = sortSelect.value.split('-');
+ const apiSortBy = {
+ name: 'name',
+ reports: 'reports',
+ posted: 'age'
+ }[sortBy] || 'age';
- let apiSortBy;
-
- switch (sortBy) {
- case 'name':
- apiSortBy = 'name';
- break;
- case 'reports':
- apiSortBy = 'reports';
- break;
- case 'posted':
- apiSortBy = 'age';
- break;
- default:
- apiSortBy = 'age';
- }
-
- return { sort_by: apiSortBy, sort_order: sortOrder };
+ return { sort_by: apiSortBy, sort_order: sortOrder || 'desc' };
}
-
- // init call to fetch programs
- const sortSelect = document.getElementById('sort-select');
+ // Event listeners
sortSelect.addEventListener('change', () => fetchPrograms(true));
+ container.addEventListener('click', handleCardClick);
+ clearBtn.addEventListener('click', clearAllSelections);
+
+ function handleCardClick(event) {
+ const card = event.target.closest('.card-selectable');
+ if (card) {
+ if (event.target.closest('.btn-see-details') || event.target.closest('.handle-link')) {
+ // Handle "See details" button or handle link click
+ return;
+ }
+ toggleCardSelection(card);
+ }
+ }
+
+ function toggleCardSelection(card) {
+ card.classList.toggle('card-selected');
+ updateImportButton();
+ }
+
+ function updateImportButton() {
+ const selectedCards = container.querySelectorAll('.card-selected');
+ const count = selectedCards.length;
+
+ importBtn.disabled = count === 0;
+ importBtn.innerHTML = count === 0 ? '
Import Programs' : `
Import ${count} Program${count !== 1 ? 's' : ''}`;
+ clearBtn.style.display = count === 0 ? 'none' : 'inline';
+ }
+
+ function clearAllSelections() {
+ container.querySelectorAll('.card-selected').forEach(card => card.classList.remove('card-selected'));
+ updateImportButton();
+ }
+
+ function showLoadingIndicator(message) {
+ Swal.fire({
+ title: message,
+ text: "Please wait",
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ showConfirmButton: false,
+ willOpen: () => {
+ Swal.showLoading();
+ }
+ });
+ }
+
+ function hideLoadingIndicator() {
+ Swal.close();
+ }
fetchPrograms(false);
-
});
\ No newline at end of file