YSWS-Catalog/script.js
2025-01-12 17:05:30 -05:00

523 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let programs = {};
const apiUrl = "https://api2.hackclub.com/v0.1/Unified%20YSWS%20Projects%20DB/YSWS%20Programs?cache=true";
var participants = []
async function startRender() {
await Promise.all([loadParticipants(), loadPrograms()]);
renderPrograms();
}
startRender()
function loadParticipants() {
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["UnweightedTotal"]
}));
console.log(participants);
})
.catch(error => {
console.error("Error fetching data:", error);
});
}
loadParticipants()
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) {
if (name === undefined) return '';
return getParticipantsByName(name)
}
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 ?
`<div class="program-participants">${formatParticipants(program.name)}</div>` : '';
return `
<div class="card program-card ${opensClass}" data-program="${encodedProgram}">
<div class="program-header">
<h3>${program.name}</h3>
<span class="program-status status-${program.status}">${program.status}</span>
</div>
<p>${program.description}</p>
<div class="program-deadline ${deadlineClass}">${deadlineText}</div>
${participantsText}
<div class="program-links">
${program.website ? `<a href="${program.website}" target="_blank">Website</a>` : ''}
${program.slack ? `<a href="${program.slack}" target="_blank">${program.slackChannel}</a>` : ''}
</div>
</div>
`;
}
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 <a href="${program.website}" target="_blank">program website</a>` : null,
program.slack ? `Join the discussion in <a href="${program.slack}" target="_blank">${program.slackChannel}</a>` : null
].filter(Boolean);
const steps = program.steps || defaultSteps;
modal.querySelector('.participation-steps').innerHTML = steps
.map((step, index) => `${index + 1}. ${step}`)
.join('<br>');
const moreDetailsElement = modal.querySelector('.more-details');
let detailsHTML = '';
if (program.participants !== undefined) {
detailsHTML += `
<h3>Participation</h3>
<p>${formatParticipants(program.name)}</p>
`;
}
if (program.requirements?.length) {
detailsHTML += `
<h3>Requirements</h3>
<ul>
${program.requirements.map(req => `<li>${req}</li>`).join('')}
</ul>
`;
}
if (program.details?.length) {
detailsHTML += `
<h3>Additional Details</h3>
<ul>
${program.details.map(detail => `<li>${detail}</li>`).join('')}
</ul>
`;
}
moreDetailsElement.innerHTML = detailsHTML;
const links = [];
if (program.website) links.push(`<a href="${program.website}" target="_blank">Website</a>`);
if (program.slack) links.push(`<a href="${program.slack}" target="_blank">${program.slackChannel}</a>`);
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 = `
<h2 class="headline">${category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</h2>
<div class="programs-grid">
${programsList.map(program => createProgramCard(program)).join('')}
</div>
`;
container.appendChild(section);
}
} else {
const sortedPrograms = sortPrograms(programs, currentSort);
const section = document.createElement('section');
section.className = 'category-section';
section.innerHTML = `
<div class="programs-grid">
${sortedPrograms.map(program => createProgramCard(program)).join('')}
</div>
`;
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', () => {
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.querySelectorAll('.sort-btn').forEach(button => {
button.addEventListener('click', () => {
updateSort(button.dataset.sort);
});
});
});
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));
});