mirror of
https://github.com/System-End/YSWS-Catalog.git
synced 2026-04-19 22:15:06 +00:00
Add program completion tracking with localStorage
This commit is contained in:
parent
a424c602e6
commit
bec7fac2aa
3 changed files with 284 additions and 5 deletions
10
index.html
10
index.html
|
|
@ -251,6 +251,10 @@
|
|||
<span class="program-status"></span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="modal-completion-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
You've completed this program
|
||||
</span>
|
||||
<p class="program-description"></p>
|
||||
<div class="program-deadline"></div>
|
||||
<div class="program-details">
|
||||
|
|
@ -259,6 +263,12 @@
|
|||
<div class="more-details"></div>
|
||||
<div class="program-links"></div>
|
||||
</div>
|
||||
<div class="modal-completion-container">
|
||||
<button class="modal-completion-toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>
|
||||
Mark as completed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
122
script.js
122
script.js
|
|
@ -2,8 +2,78 @@ let programs = {};
|
|||
const apiUrl = "https://api2.hackclub.com/v0.1/Unified%20YSWS%20Projects%20DB/YSWS%20Programs?cache=true";
|
||||
let participants = [];
|
||||
let initialParticipants = new Map();
|
||||
let completedPrograms = new Set();
|
||||
|
||||
function loadCompletedPrograms() {
|
||||
const saved = localStorage.getItem('completedPrograms');
|
||||
if (saved) {
|
||||
completedPrograms = new Set(JSON.parse(saved));
|
||||
}
|
||||
}
|
||||
|
||||
function saveCompletedPrograms() {
|
||||
localStorage.setItem('completedPrograms', JSON.stringify([...completedPrograms]));
|
||||
}
|
||||
|
||||
function toggleProgramCompletion(programName, event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (completedPrograms.has(programName)) {
|
||||
completedPrograms.delete(programName);
|
||||
} else {
|
||||
completedPrograms.add(programName);
|
||||
}
|
||||
|
||||
saveCompletedPrograms();
|
||||
updateCompletionUI(programName);
|
||||
}
|
||||
|
||||
function updateCompletionUI(programName) {
|
||||
const isCompleted = completedPrograms.has(programName);
|
||||
|
||||
document.querySelectorAll(`.program-card[data-name="${programName}"]`).forEach(card => {
|
||||
const completionBtn = card.querySelector('.program-completion-toggle');
|
||||
const completionBadge = card.querySelector('.user-completed-badge');
|
||||
|
||||
if (completionBtn) {
|
||||
completionBtn.innerHTML = isCompleted ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>';
|
||||
|
||||
completionBtn.setAttribute('aria-label', isCompleted ? 'Mark as not completed' : 'Mark as completed');
|
||||
completionBtn.classList.toggle('completed', isCompleted);
|
||||
}
|
||||
|
||||
if (completionBadge) {
|
||||
completionBadge.classList.toggle('visible', isCompleted);
|
||||
}
|
||||
});
|
||||
|
||||
const modal = document.getElementById('program-modal');
|
||||
if (modal.classList.contains('active')) {
|
||||
const modalTitle = modal.querySelector('.title').textContent;
|
||||
if (modalTitle === programName) {
|
||||
const modalCompletionBtn = modal.querySelector('.modal-completion-toggle');
|
||||
if (modalCompletionBtn) {
|
||||
modalCompletionBtn.innerHTML = isCompleted ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg> Completed' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg> Mark as completed';
|
||||
|
||||
modalCompletionBtn.classList.toggle('completed', isCompleted);
|
||||
}
|
||||
|
||||
const modalCompletionBadge = modal.querySelector('.modal-completion-badge');
|
||||
if (modalCompletionBadge) {
|
||||
modalCompletionBadge.classList.toggle('visible', isCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startRender() {
|
||||
loadCompletedPrograms();
|
||||
await loadPrograms();
|
||||
Object.values(programs).flat().forEach(program => {
|
||||
if (program.participants !== undefined) {
|
||||
|
|
@ -229,21 +299,38 @@ function createProgramCard(program) {
|
|||
|
||||
const encodedProgram = encodeURIComponent(JSON.stringify(program));
|
||||
|
||||
const isCompletedByUser = completedPrograms.has(program.name);
|
||||
const completionButtonClass = isCompletedByUser ? 'completed' : '';
|
||||
const completionIcon = isCompletedByUser ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>';
|
||||
|
||||
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="card program-card ${opensClass}" data-program="${encodedProgram}" data-name="${program.name}">
|
||||
<div class="program-header">
|
||||
<h3>${program.name}</h3>
|
||||
<span class="program-status status-${program.status}">${program.status}</span>
|
||||
<div class="status-container">
|
||||
<span class="user-completed-badge ${isCompletedByUser ? 'visible' : ''}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
You completed this
|
||||
</span>
|
||||
<span class="program-status status-${program.status}">${program.status}</span>
|
||||
</div>
|
||||
</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 class="program-footer">
|
||||
<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>
|
||||
<button class="program-completion-toggle ${completionButtonClass}" aria-label="${isCompletedByUser ? 'Mark as not completed' : 'Mark as completed'}" data-program-name="${program.name}">
|
||||
${completionIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -343,6 +430,17 @@ function openModal(program) {
|
|||
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(' | ');
|
||||
|
||||
const isCompletedByUser = completedPrograms.has(program.name);
|
||||
const modalCompletionBtn = modal.querySelector('.modal-completion-toggle');
|
||||
modalCompletionBtn.innerHTML = isCompletedByUser ?
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg> Completed' :
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg> Mark as completed';
|
||||
modalCompletionBtn.classList.toggle('completed', isCompletedByUser);
|
||||
modalCompletionBtn.dataset.programName = program.name;
|
||||
|
||||
const modalCompletionBadge = modal.querySelector('.modal-completion-badge');
|
||||
modalCompletionBadge.classList.toggle('visible', isCompletedByUser);
|
||||
|
||||
updatePositionIndicator();
|
||||
modal.classList.add('active');
|
||||
|
|
@ -570,6 +668,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.program-completion-toggle')) {
|
||||
const button = e.target.closest('.program-completion-toggle');
|
||||
const programName = button.dataset.programName;
|
||||
toggleProgramCompletion(programName, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.closest('.modal-completion-toggle')) {
|
||||
const button = e.target.closest('.modal-completion-toggle');
|
||||
const programName = button.dataset.programName;
|
||||
toggleProgramCompletion(programName, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.closest('.program-card') && e.target.closest('a')) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
157
styles.css
157
styles.css
|
|
@ -1944,4 +1944,161 @@ html {
|
|||
|
||||
.rss-link:hover svg {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.program-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
.program-completion-toggle {
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: var(--text);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-left: var(--spacing-2);
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark-theme .program-completion-toggle {
|
||||
background: rgba(23, 23, 29, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.program-completion-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
background: rgba(51, 214, 166, 0.2);
|
||||
box-shadow: 0 0 10px rgba(51, 214, 166, 0.3);
|
||||
}
|
||||
|
||||
.program-completion-toggle.completed {
|
||||
background: var(--green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.program-completion-toggle.completed:hover {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.user-completed-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: var(--font-1);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--green);
|
||||
background: rgba(51, 214, 166, 0.1);
|
||||
border: 1px solid var(--green);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radii-circle);
|
||||
margin-right: var(--spacing-2);
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.user-completed-badge.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-completion-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: var(--font-2);
|
||||
font-weight: var(--font-weight-bold);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--radii-circle);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-top: var(--spacing-3);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.dark-theme .modal-completion-toggle {
|
||||
background: rgba(23, 23, 29, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.modal-completion-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(51, 214, 166, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(51, 214, 166, 0.2);
|
||||
}
|
||||
|
||||
.modal-completion-toggle.completed {
|
||||
background: var(--green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-completion-toggle.completed:hover {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.modal-completion-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: var(--font-2);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--green);
|
||||
background: rgba(51, 214, 166, 0.1);
|
||||
border: 1px solid var(--green);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--radii-circle);
|
||||
margin-bottom: var(--spacing-3);
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.modal-completion-badge.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-completion-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.status-container {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.user-completed-badge {
|
||||
margin-bottom: var(--spacing-1);
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue