mirror of
https://github.com/System-End/YSWS-Catalog.git
synced 2026-04-19 23:23:02 +00:00
Add program modal and filter for 'Ending Soon' programs
This commit is contained in:
parent
54be7d2fa5
commit
485029f01b
3 changed files with 227 additions and 5 deletions
21
index.html
21
index.html
|
|
@ -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">×</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
117
script.js
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
94
styles.css
94
styles.css
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue