let programs = {};
const apiUrl = "https://api2.hackclub.com/v0.1/Unified%20YSWS%20Projects%20DB/YSWS%20Programs?cache=true";
let participants = [];
let initialParticipants = new Map();
async function startRender() {
await loadPrograms();
Object.values(programs).flat().forEach(program => {
if (program.participants !== undefined) {
initialParticipants.set(program.name, program.participants);
}
});
renderPrograms();
await loadParticipants();
updateParticipantCounts();
}
function loadParticipants() {
return fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to Fetch Participants Data! ${response.status}`);
}
return response.json();
})
.then(data => {
participants = data.map(item => ({
name: item.fields.Name,
total: item.fields["Unweighted–Total"],
id: item.id
}));
})
.catch(error => {
console.error("Error fetching data:", error);
});
}
const unifiedDbOverrides = {
"HackCraft": "recE2drMuGXUWJi3L",
};
function animateNumber(element, start, end, duration = 1000) {
const startTime = performance.now();
const startNum = parseInt(start) || 0;
const endNum = parseInt(end) || 0;
const numberSpan = element.querySelector('span');
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuad = 1 - Math.pow(1 - progress, 2);
const current = Math.round(startNum + (endNum - startNum) * easeOutQuad);
numberSpan.textContent = current;
element.textContent = `${current} participant${current !== 1 ? 's' : ''}`;
if (progress < 1) {
requestAnimationFrame(update);
} else {
element.classList.remove('updating');
}
}
element.classList.add('updating');
requestAnimationFrame(update);
}
function updateParticipantCounts() {
const participantElements = document.querySelectorAll('.program-participants');
participantElements.forEach(element => {
const programCard = element.closest('.program-card');
const programData = JSON.parse(decodeURIComponent(programCard.dataset.program));
const programName = programData.name;
const overrideId = unifiedDbOverrides[programName];
const apiData = overrideId
? participants.find(p => p.id === overrideId)
: participants.find(p => p.name === programName);
if (apiData) {
const initialCount = initialParticipants.get(programName) || 0;
animateNumber(element, initialCount, apiData.total);
}
});
}
function getParticipantsByName(programName) {
if (!participants.length) {
console.error("Data has not been fetched yet. Please wait...");
return;
}
const program = participants.find(item => item.name.toLowerCase() === programName.toLowerCase());
if (program) {
console.log(`Program: ${program.name}, Participants: ${program.total}`);
return program.total;
} else {
console.log(`Program "${programName}" not found.`);
return null;
}
}
function isEventEnded(deadline) {
if (!deadline) return false;
const now = new Date();
const deadlineDate = new Date(deadline);
return now > deadlineDate;
}
async function loadPrograms() {
try {
const response = await fetch('data.yml').then(res => res.text());
const rawPrograms = jsyaml.load(response);
const completed = [];
programs = Object.fromEntries(
Object.entries(rawPrograms).map(([category, programsList]) => [
category,
programsList.filter(program => {
if (program.status === 'completed' || isEventEnded(program.deadline)) {
completed.push({ ...program, status: 'completed' });
return false;
}
return true;
})
])
);
delete programs['Completed'];
if (completed.length > 0) {
programs['Completed'] = completed;
}
programs = Object.fromEntries(
Object.entries(programs).filter(([_, programsList]) => programsList.length > 0)
);
} 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 < 0) return 'ended';
if (diffDays <= 7) return 'very-urgent';
if (diffDays <= 14) return 'urgent';
return '';
}
function formatParticipants(name) {
const initial = initialParticipants.get(name);
if (initial === undefined) return '';
return `${initial} participant${initial !== 1 ? 's' : ''}`;
}
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));
const participantsText = program.participants !== undefined ?
`
${formatParticipants(program.name)}
` : '';
return `
${program.description}
${deadlineText}
${participantsText}
`;
}
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 updatePositionIndicator() {
const positionElement = document.querySelector('.current-position');
if (visiblePrograms.length > 0) {
positionElement.textContent = `${currentProgramIndex + 1} of ${visiblePrograms.length}`;
} else {
positionElement.textContent = '';
}
}
function navigateModal(direction) {
updateVisiblePrograms();
if (visiblePrograms.length === 0) return;
currentProgramIndex = (currentProgramIndex + direction + visiblePrograms.length) % visiblePrograms.length;
openModal(visiblePrograms[currentProgramIndex]);
updatePositionIndicator();
}
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.participants !== undefined) {
detailsHTML += `
Participation
${formatParticipants(program.name)}
`;
}
if (program.requirements?.length) {
detailsHTML += `
Requirements
${program.requirements.map(req => `- ${req}
`).join('')}
`;
}
if (program.details?.length) {
detailsHTML += `
Additional Details
${program.details.map(detail => `- ${detail}
`).join('')}
`;
}
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(' | ');
updatePositionIndicator();
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;
}
let currentSort = 'default';
function sortPrograms(programs, sortType) {
const flattened = Object.entries(programs).flatMap(([category, progs]) =>
progs.map(p => ({...p, category}))
);
switch(sortType) {
case 'alphabetical':
return flattened.sort((a, b) => a.name.localeCompare(b.name));
case 'deadline':
return flattened.sort((a, b) => {
if (!a.deadline) return 1;
if (!b.deadline) return -1;
return new Date(a.deadline) - new Date(b.deadline);
});
case 'status':
const statusOrder = { active: 0, draft: 1, completed: 2 };
return flattened.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
default:
return flattened;
}
}
function renderPrograms() {
const container = document.getElementById('programs-container');
container.innerHTML = '';
const activeCount = countActivePrograms();
document.getElementById('active-count').textContent = activeCount;
if (currentSort === 'default') {
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);
}
} else {
const sortedPrograms = sortPrograms(programs, currentSort);
const section = document.createElement('section');
section.className = 'category-section';
section.innerHTML = `
${sortedPrograms.map(program => createProgramCard(program)).join('')}
`;
container.appendChild(section);
}
}
function updateSort(sortType) {
currentSort = sortType;
const buttons = document.querySelectorAll('.sort-btn');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.sort === sortType);
});
renderPrograms();
const activeFilter = document.querySelector('.filter-btn.active');
if (activeFilter) {
filterPrograms(activeFilter.dataset.category);
}
const searchInput = document.getElementById('program-search');
if (searchInput.value) {
searchPrograms(searchInput.value);
}
}
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');
let needsReload = false;
deadlineElements.forEach(element => {
const card = element.closest('.program-card');
const programData = JSON.parse(decodeURIComponent(card.dataset.program));
if (programData?.deadline) {
if (isEventEnded(programData.deadline) && programData.status !== 'completed') {
needsReload = true;
return;
}
const deadlineText = formatDeadline(programData.deadline, programData.opens);
const deadlineClass = getDeadlineClass(programData.deadline);
element.textContent = deadlineText;
element.className = `program-deadline ${deadlineClass}`;
}
});
if (needsReload) {
window.location.reload();
}
}
document.addEventListener('DOMContentLoaded', () => {
startRender();
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.querySelectorAll('.sort-btn').forEach(button => {
button.addEventListener('click', () => {
updateSort(button.dataset.sort);
});
});
document.addEventListener('click', (e) => {
if (e.target.closest('.program-card') && e.target.closest('a')) {
return;
}
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));
});