Add program modal and filter for 'Ending Soon' programs

This commit is contained in:
PawiX25 2024-12-11 20:00:05 +01:00
parent 54be7d2fa5
commit 485029f01b
3 changed files with 227 additions and 5 deletions

View file

@ -24,12 +24,33 @@
<div class="filter-container">
<button class="filter-btn active" data-category="all">All</button>
<button class="filter-btn" data-category="active">Active</button>
<button class="filter-btn" data-category="ending-soon">Ending Soon</button>
<button class="filter-btn" data-category="upcoming">Upcoming</button>
<button class="filter-btn" data-category="completed">Completed</button>
</div>
<div id="programs-container">
</div>
<div id="program-modal" class="modal">
<div class="modal-content card">
<button class="modal-close" aria-label="Close modal">&times;</button>
<div class="modal-header">
<h2 class="title"></h2>
<span class="program-status"></span>
</div>
<div class="modal-body">
<p class="program-description"></p>
<div class="program-deadline"></div>
<div class="program-details">
<h3>How to Participate</h3>
<div class="participation-steps"></div>
<div class="more-details"></div>
<div class="program-links"></div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>

117
script.js
View file

@ -10,7 +10,19 @@ async function loadPrograms() {
}
}
function formatDeadline(deadlineStr) {
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);
@ -48,17 +60,21 @@ function getDeadlineClass(deadlineStr) {
}
function createProgramCard(program) {
const deadlineText = formatDeadline(program.deadline);
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 `
<div class="card program-card">
<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>
${program.deadline ? `<div class="program-deadline ${deadlineClass}">${deadlineText}</div>` : ''}
<div class="program-deadline ${deadlineClass}">${deadlineText}</div>
<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>` : ''}
@ -67,6 +83,74 @@ function createProgramCard(program) {
`;
}
function openModal(program) {
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.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(' | ');
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 => {
@ -106,10 +190,16 @@ function filterPrograms(category) {
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);
}
@ -178,7 +268,7 @@ function updateDeadlines() {
.find(p => p.name === programName);
if (program?.deadline) {
const deadlineText = formatDeadline(program.deadline);
const deadlineText = formatDeadline(program.deadline, program.opens);
const deadlineClass = getDeadlineClass(program.deadline);
element.textContent = deadlineText;
@ -204,4 +294,21 @@ document.addEventListener('DOMContentLoaded', () => {
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 (e.key === 'Escape') closeModal();
});
});

View file

@ -582,6 +582,7 @@ td {
flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
cursor: pointer;
}
.program-card:hover {
@ -590,6 +591,16 @@ td {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.program-card.opens-soon {
border: 2px dashed var(--blue);
background: linear-gradient(rgba(51, 142, 218, 0.05), rgba(51, 142, 218, 0.05));
}
.program-card.opens-soon .program-deadline {
color: var(--blue);
font-weight: var(--font-weight-bold);
}
.program-header {
display: flex;
justify-content: space-between;
@ -603,12 +614,21 @@ td {
border-radius: var(--radii-small);
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 70px;
text-align: center;
height: 24px;
line-height: 1;
}
.status-active {
background-color: var(--green);
color: var(--white);
animation: status-glow 2s ease-in-out infinite;
min-width: 55px;
font-size: 11px;
}
@keyframes status-glow {
@ -765,6 +785,80 @@ td {
display: none;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
}
.modal-content {
position: relative;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
transform: translateY(-20px);
transition: transform 0.3s ease;
padding: var(--spacing-4);
}
.modal-close {
position: absolute;
top: var(--spacing-3);
right: var(--spacing-3);
background: none;
border: none;
font-size: var(--font-4);
color: var(--text);
padding: var(--spacing-1);
cursor: pointer;
width: 32px;
height: 32px;
box-shadow: none;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-3);
padding-right: 40px;
}
.modal-body {
margin-top: var(--spacing-3);
}
.program-details {
margin-top: var(--spacing-4);
}
.participation-steps {
margin: var(--spacing-3) 0;
}
body.modal-open {
overflow: hidden;
}
@media screen and (min-width: 32em) {
.ultratitle {
font-size: var(--font-5);