Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions migrations/0005_add_certificates.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Migration 0005: Add certificates table

CREATE TABLE IF NOT EXISTS certificates (
id TEXT PRIMARY KEY,
enrollment_id TEXT NOT NULL UNIQUE,
issued_at TEXT NOT NULL DEFAULT (datetime('now')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (enrollment_id) REFERENCES enrollments(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_certificates_enrollment ON certificates(enrollment_id);
102 changes: 102 additions & 0 deletions public/activity.html
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ <h2 class="font-bold text-lg text-gray-800 dark:text-gray-200 mb-4 flex items-ce
Loading description...
</p>
<div id="act-tags" class="flex flex-wrap gap-2"></div>
<div id="certificate-section" class="hidden mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 class="font-bold text-base text-gray-800 dark:text-gray-200 mb-3 flex items-center gap-2">
<i class="fas fa-certificate text-teal-600 dark:text-teal-400"></i>Certificate
</h3>
<div class="space-y-3">
<button id="btn-certificate" type="button" class="w-full inline-flex items-center justify-center gap-2 font-bold px-5 py-3 rounded-xl transition-all"></button>
<p id="certificate-note" class="text-sm text-gray-600 dark:text-gray-300"></p>
<p id="certificate-error" class="hidden text-sm text-red-600 dark:text-red-400"></p>
</div>
</div>
</div>

<!-- Sessions Card -->
Expand Down Expand Up @@ -310,6 +320,7 @@ <h2 class="font-bold text-lg text-gray-800 dark:text-gray-200 mb-4 flex items-ce
let allActivities = [];
let currentActivity = null;
let activeTag = null;
let currentEnrollment = null;

// ==================== VIEW SWITCHING ====================
function showBrowser() {
Expand Down Expand Up @@ -456,7 +467,95 @@ <h3 class="font-bold text-gray-800 dark:text-gray-200 text-base leading-snug">${
}).join('');
}

function resetCertificateSection() {
const section = document.getElementById('certificate-section');
const button = document.getElementById('btn-certificate');
const note = document.getElementById('certificate-note');
const error = document.getElementById('certificate-error');
section.classList.add('hidden');
error.classList.add('hidden');
error.textContent = '';
note.textContent = '';
button.disabled = false;
button.onclick = null;
button.className = 'w-full inline-flex items-center justify-center gap-2 font-bold px-5 py-3 rounded-xl transition-all';
}

function renderCertificateSection(enrollment) {
const section = document.getElementById('certificate-section');
const button = document.getElementById('btn-certificate');
const note = document.getElementById('certificate-note');
const error = document.getElementById('certificate-error');

resetCertificateSection();
section.classList.remove('hidden');

if (enrollment.status === 'completed') {
button.innerHTML = '<i class="fas fa-certificate"></i>Generate Certificate';
button.classList.add('bg-green-600', 'hover:bg-green-700', 'text-white', 'shadow-md');
note.textContent = 'Your activity is complete. Generate your certificate and download it as a PDF.';
button.onclick = generateCertificate;
return;
}

button.innerHTML = '<i class="fas fa-lock"></i>Certificate not available yet';
button.disabled = true;
button.classList.add('bg-gray-300', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-300', 'cursor-not-allowed', 'opacity-60');
note.textContent = 'Complete all sessions in this activity to unlock your certificate.';
error.classList.add('hidden');
}

async function generateCertificate() {
const error = document.getElementById('certificate-error');
const button = document.getElementById('btn-certificate');
error.classList.add('hidden');
error.textContent = '';

if (!token) {
window.location.href = '/login.html';
return;
}
if (!currentEnrollment || !currentEnrollment.id) {
error.textContent = 'Enrollment not found for certificate generation.';
error.classList.remove('hidden');
return;
}

const originalHtml = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>Generating...';

try {
const res = await fetch(`/api/certificates/generate/${encodeURIComponent(currentEnrollment.id)}`, {
method: 'POST',
headers: { Authorization: 'Bearer ' + token }
});
const payload = await res.json();

if (res.ok) {
window.open('/certificate.html?uuid=' + encodeURIComponent(payload.data.uuid), '_blank');
button.disabled = false;
button.innerHTML = originalHtml;
return;
}

if (res.status === 400) {
error.textContent = payload.error || 'Something went wrong. Please try again.';
} else {
error.textContent = 'Something went wrong. Please try again.';
}
error.classList.remove('hidden');
} catch (e) {
error.textContent = 'Something went wrong. Please try again.';
error.classList.remove('hidden');
}

button.disabled = false;
button.innerHTML = originalHtml;
}

async function loadActivityDetail(activityId) {
resetCertificateSection();
const headers = token ? { Authorization: 'Bearer ' + token } : {};
const res = await fetch('/api/activities/' + activityId, { headers });
const data = await res.json();
Expand Down Expand Up @@ -538,6 +637,7 @@ <h3 class="font-bold text-gray-800 dark:text-gray-200 text-base leading-snug">${
document.getElementById('join-cta').classList.add('hidden');
document.getElementById('member-card').classList.remove('hidden');
const enr = data.enrollment;
currentEnrollment = enr;
const rc = { participant:'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', instructor:'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300', organizer:'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' };
const sc = { active:'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300', cancelled:'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300', completed:'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' };
document.getElementById('enr-details').innerHTML =
Expand All @@ -547,7 +647,9 @@ <h3 class="font-bold text-gray-800 dark:text-gray-200 text-base leading-snug">${
document.getElementById('act-action').innerHTML = '<span class="inline-flex items-center gap-2 text-teal-600 dark:text-teal-400 text-sm font-semibold">✅ Joined</span>';
document.getElementById('welcome-card').querySelector('#welcome-text').textContent =
'You are participating in this activity. Session locations and details are visible above.';
renderCertificateSection(enr);
} else {
currentEnrollment = null;
if (token) {
document.getElementById('join-cta').classList.remove('hidden');
} else {
Expand Down
Loading
Loading