let programs = {}; async function loadPrograms() { try { const response = await fetch('programs.json'); programs = await response.json(); renderPrograms(); } catch (error) { console.error('Error loading programs:', error); } } function formatDeadline(deadlineStr, opensStr) { if (opensStr) { const opensDate = new Date(opensStr); const now = new Date(); if (now < opensDate) { return `Opens ${opensDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: opensDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined })}`; } } if (!deadlineStr) return ''; const deadline = new Date(deadlineStr); const now = new Date(); const diffTime = deadline - now; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays < 0) return 'Ended'; if (diffDays === 0) return 'Ends today'; if (diffDays === 1) return 'Ends tomorrow'; if (diffDays <= 7) return `${diffDays} days left`; if (diffDays <= 30) { const weeks = Math.floor(diffDays / 7); return `${weeks} week${weeks > 1 ? 's' : ''} left`; } return `Ends ${deadline.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: deadline.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined })}`; } function getDeadlineClass(deadlineStr) { if (!deadlineStr) return ''; const deadline = new Date(deadlineStr); const now = new Date(); const diffTime = deadline - now; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays <= 2) return 'very-urgent'; if (diffDays <= 7) return 'urgent'; return ''; } function createProgramCard(program) { const deadlineText = formatDeadline(program.deadline, program.opens); const deadlineClass = getDeadlineClass(program.deadline); const opensClass = program.opens && new Date() < new Date(program.opens) ? 'opens-soon' : ''; const encodedProgram = encodeURIComponent(JSON.stringify(program)); return `

${program.name}

${program.status}

${program.description}

${deadlineText}
`; } let currentProgramIndex = 0; let visiblePrograms = []; function updateVisiblePrograms() { visiblePrograms = Array.from(document.querySelectorAll('.program-card')) .filter(card => !card.classList.contains('hidden-by-filter') && !card.classList.contains('hidden-by-search')) .map(card => JSON.parse(decodeURIComponent(card.dataset.program))); } function navigateModal(direction) { updateVisiblePrograms(); if (visiblePrograms.length === 0) return; currentProgramIndex = (currentProgramIndex + direction + visiblePrograms.length) % visiblePrograms.length; openModal(visiblePrograms[currentProgramIndex]); } function openModal(program) { updateVisiblePrograms(); currentProgramIndex = visiblePrograms.findIndex(p => p.name === program.name); const modal = document.getElementById('program-modal'); const body = document.body; modal.querySelector('.title').textContent = program.name; modal.querySelector('.program-status').className = `program-status status-${program.status}`; modal.querySelector('.program-status').textContent = program.status; modal.querySelector('.program-description').textContent = program.detailedDescription || program.description; const deadlineElement = modal.querySelector('.program-deadline'); const deadlineText = formatDeadline(program.deadline, program.opens); const deadlineClass = getDeadlineClass(program.deadline); deadlineElement.className = `program-deadline ${deadlineClass}`; deadlineElement.textContent = deadlineText; const defaultSteps = [ program.website ? `Visit the program website` : null, program.slack ? `Join the discussion in ${program.slackChannel}` : null ].filter(Boolean); const steps = program.steps || defaultSteps; modal.querySelector('.participation-steps').innerHTML = steps .map((step, index) => `${index + 1}. ${step}`) .join('
'); const moreDetailsElement = modal.querySelector('.more-details'); let detailsHTML = ''; if (program.requirements?.length) { detailsHTML += `

Requirements

`; } if (program.details?.length) { detailsHTML += `

Additional Details

`; } moreDetailsElement.innerHTML = detailsHTML; const links = []; if (program.website) links.push(`Website`); if (program.slack) links.push(`${program.slackChannel}`); modal.querySelector('.program-links').innerHTML = links.join(' | '); modal.classList.add('active'); body.classList.add('modal-open'); } function closeModal() { const modal = document.getElementById('program-modal'); const body = document.body; modal.classList.remove('active'); body.classList.remove('modal-open'); } function countActivePrograms() { let count = 0; Object.values(programs).forEach(category => { count += category.filter(program => program.status === 'active').length; }); return count; } function renderPrograms() { const container = document.getElementById('programs-container'); const activeCount = countActivePrograms(); document.getElementById('active-count').textContent = activeCount; for (const [category, programsList] of Object.entries(programs)) { const section = document.createElement('section'); section.className = 'category-section'; section.innerHTML = `

${category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}

${programsList.map(program => createProgramCard(program)).join('')}
`; container.appendChild(section); } } function filterPrograms(category) { const sections = document.querySelectorAll('.category-section'); const buttons = document.querySelectorAll('.filter-btn'); buttons.forEach(btn => { btn.classList.toggle('active', btn.dataset.category === category); }); sections.forEach(section => { const programCards = section.querySelectorAll('.program-card'); programCards.forEach(card => { const statusElement = card.querySelector('.program-status'); const deadlineElement = card.querySelector('.program-deadline'); const status = statusElement.textContent; if (category === 'all') { card.classList.remove('hidden-by-filter'); } else if (category === 'ending-soon') { const isEndingSoon = deadlineElement && ['urgent', 'very-urgent'].some(cls => deadlineElement.classList.contains(cls)); card.classList.toggle('hidden-by-filter', !isEndingSoon); } else { card.classList.toggle('hidden-by-filter', status !== category); } }); const hasVisibleCards = Array.from(programCards) .some(card => !card.classList.contains('hidden-by-filter') && !card.classList.contains('hidden-by-search')); section.classList.toggle('hidden', !hasVisibleCards); }); } function searchPrograms(searchTerm) { const programCards = document.querySelectorAll('.program-card'); searchTerm = searchTerm.toLowerCase().trim(); programCards.forEach(card => { const name = card.querySelector('h3').textContent.toLowerCase(); const description = card.querySelector('p').textContent.toLowerCase(); const slackChannel = card.querySelector('.program-links')?.textContent.toLowerCase() || ''; const matches = name.includes(searchTerm) || description.includes(searchTerm) || slackChannel.includes(searchTerm); card.classList.toggle('hidden-by-search', !matches); }); const sections = document.querySelectorAll('.category-section'); sections.forEach(section => { const hasVisibleCards = Array.from(section.querySelectorAll('.program-card')) .some(card => !card.classList.contains('hidden-by-filter') && !card.classList.contains('hidden-by-search')); section.classList.toggle('hidden', !hasVisibleCards); }); } function toggleTheme() { const body = document.body; const toggleBtn = document.getElementById('theme-toggle'); const isDark = body.classList.toggle('dark-theme'); toggleBtn.textContent = isDark ? '☀️' : '🌙'; localStorage.setItem('theme', isDark ? 'dark' : 'light'); } function initializeTheme() { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const toggleBtn = document.getElementById('theme-toggle'); if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.body.classList.add('dark-theme'); toggleBtn.textContent = '☀️'; } } function updateDeadlines() { const deadlineElements = document.querySelectorAll('.program-deadline'); deadlineElements.forEach(element => { const card = element.closest('.program-card'); const programName = card.querySelector('h3').textContent; const program = Object.values(programs) .flat() .find(p => p.name === programName); if (program?.deadline) { const deadlineText = formatDeadline(program.deadline, program.opens); const deadlineClass = getDeadlineClass(program.deadline); element.textContent = deadlineText; element.className = `program-deadline ${deadlineClass}`; } }); } document.addEventListener('DOMContentLoaded', () => { loadPrograms().then(() => { const searchInput = document.getElementById('program-search'); searchInput.addEventListener('input', (e) => searchPrograms(e.target.value)); document.querySelectorAll('.filter-btn').forEach(button => { button.addEventListener('click', () => { filterPrograms(button.dataset.category); searchPrograms(searchInput.value); }); }); initializeTheme(); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); setInterval(updateDeadlines, 60000); }); document.addEventListener('click', (e) => { if (e.target.closest('.program-card')) { const encodedProgram = e.target.closest('.program-card').dataset.program; const program = JSON.parse(decodeURIComponent(encodedProgram)); openModal(program); } if (e.target.closest('.modal-close') || (e.target.classList.contains('modal') && !e.target.closest('.modal-content'))) { closeModal(); } }); document.addEventListener('keydown', (e) => { if (!document.getElementById('program-modal').classList.contains('active')) return; switch(e.key) { case 'Escape': closeModal(); break; case 'ArrowLeft': navigateModal(-1); break; case 'ArrowRight': navigateModal(1); break; } }); document.querySelector('.modal-prev').addEventListener('click', () => navigateModal(-1)); document.querySelector('.modal-next').addEventListener('click', () => navigateModal(1)); });