Skip to content

Commit d91488d

Browse files
orabeCopilot
andcommitted
feat: add certificate verification page and related styles/scripts
Co-authored-by: Copilot <copilot@github.com>
1 parent 60ef7db commit d91488d

5 files changed

Lines changed: 317 additions & 0 deletions

File tree

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Python bytecode files
2+
*.pyc
3+
__pycache__/
4+
5+
# env files
6+
.env
7+
.env.*
8+
9+
# database files
10+
certificates.db
11+
12+
# venv directories
13+
venv/
14+
env/
15+
16+
*.sqlite3
17+
*.db

index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ <h1 class="name" title="MathCodeLab">MathCodeLab</h1>
195195
<button class="navbar-link" data-nav-link>Vergangene Kurse</button>
196196
</a>
197197
</li>
198+
<li class="navbar-item">
199+
<a href="verify/index.html">
200+
<button class="navbar-link" data-nav-link>Verify Certificate</button>
201+
</a>
202+
</li>
198203
</ul>
199204

200205
</nav>

verify/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Certificate Verification | MathCodeLab</title>
7+
<link rel="stylesheet" href="verify.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<header>
12+
<h1>Certificate Verification</h1>
13+
<p>Verify a MathCodeLab Certificate of Completion or Participation</p>
14+
</header>
15+
<main id="main-content">
16+
<div id="loading" class="hidden">Loading...</div>
17+
<form id="search-form" class="hidden">
18+
<label for="certificate-id">Enter Certificate ID:</label>
19+
<input type="text" id="certificate-id" name="certificate-id" maxlength="20" required pattern="MCL-\d{4}-[A-Z0-9]{6,8}">
20+
<button type="submit">Verify</button>
21+
</form>
22+
<div id="result"></div>
23+
</main>
24+
<footer>
25+
<p>&copy; <span id="year"></span> MathCodeLab</p>
26+
</footer>
27+
</div>
28+
<script src="verify.js"></script>
29+
</body>
30+
</html>

verify/verify.css

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
2+
body {
3+
font-family: var(--ff-poppins, 'Poppins', Arial, sans-serif);
4+
background: var(--bg-gradient-onyx, #f7f9fb);
5+
margin: 0;
6+
color: var(--white-1, #222);
7+
}
8+
9+
.container {
10+
max-width: 420px;
11+
margin: 48px auto;
12+
background: var(--just-white, #fff);
13+
border-radius: 18px;
14+
box-shadow: 0 8px 32px 0 rgba(39,104,237,0.10), 0 1.5px 8px 0 rgba(39,104,237,0.08);
15+
padding: 2.2rem 1.7rem 1.2rem 1.7rem;
16+
border: 1.5px solid var(--bg-gradient-yellow-2, #2668ed);
17+
}
18+
19+
header {
20+
text-align: center;
21+
margin-bottom: 1.7rem;
22+
}
23+
header h1 {
24+
font-size: 2rem;
25+
font-weight: 600;
26+
color: var(--bg-gradient-yellow-2, #2668ed);
27+
margin: 0 0 0.3em 0;
28+
letter-spacing: 0.5px;
29+
}
30+
header p {
31+
color: var(--eerie-black-1, #2668ed);
32+
font-size: 1.08rem;
33+
margin: 0;
34+
font-weight: 400;
35+
}
36+
37+
#loading {
38+
text-align: center;
39+
font-size: 1.1rem;
40+
color: var(--bg-gradient-yellow-2, #2668ed);
41+
font-weight: 500;
42+
}
43+
44+
#result {
45+
margin-top: 1.2rem;
46+
}
47+
48+
.status {
49+
font-weight: 600;
50+
font-size: 1.13rem;
51+
margin-bottom: 0.5em;
52+
letter-spacing: 0.2px;
53+
}
54+
.status.verified {
55+
color: #219150;
56+
}
57+
.status.revoked {
58+
color: #c1121f;
59+
}
60+
.status.invalid {
61+
color: #b1a7a6;
62+
}
63+
64+
.certificate-details {
65+
background: var(--bg-gradient-jet, #f1f3f6);
66+
border-radius: 10px;
67+
padding: 1.1em 1em 1em 1em;
68+
margin-bottom: 1em;
69+
border: 1px solid var(--bg-gradient-yellow-2, #2668ed10);
70+
box-shadow: 0 2px 8px 0 rgba(39,104,237,0.04);
71+
}
72+
.certificate-details dt {
73+
font-weight: 500;
74+
margin-top: 0.5em;
75+
color: var(--bg-gradient-yellow-2, #2668ed);
76+
}
77+
.certificate-details dd {
78+
margin: 0 0 0.5em 0;
79+
color: var(--white-1, #222);
80+
}
81+
82+
#search-form {
83+
display: flex;
84+
flex-direction: column;
85+
gap: 0.7em;
86+
align-items: stretch;
87+
margin-top: 1em;
88+
}
89+
#search-form label {
90+
font-size: 1.01em;
91+
color: var(--bg-gradient-yellow-2, #2668ed);
92+
font-weight: 500;
93+
margin-bottom: 0.2em;
94+
}
95+
#search-form input {
96+
padding: 0.6em 0.9em;
97+
border: 1.5px solid var(--bg-gradient-yellow-2, #2668ed);
98+
border-radius: 6px;
99+
font-size: 1em;
100+
font-family: inherit;
101+
outline: none;
102+
transition: border 0.2s;
103+
}
104+
#search-form input:focus {
105+
border-color: var(--eerie-black-1, #2668ed);
106+
}
107+
#search-form button {
108+
background: linear-gradient(90deg, var(--bg-gradient-yellow-2, #2668ed) 60%, var(--bg-gradient-yellow-1, #2668ed) 100%);
109+
color: #fff;
110+
border: none;
111+
border-radius: 6px;
112+
padding: 0.7em 0;
113+
font-size: 1.08em;
114+
font-weight: 500;
115+
cursor: pointer;
116+
box-shadow: 0 2px 8px 0 rgba(39,104,237,0.08);
117+
transition: background 0.2s, box-shadow 0.2s;
118+
}
119+
#search-form button:hover {
120+
background: linear-gradient(90deg, #1e4fa3 60%, #2668ed 100%);
121+
box-shadow: 0 4px 16px 0 rgba(39,104,237,0.13);
122+
}
123+
124+
footer {
125+
text-align: center;
126+
color: #888;
127+
font-size: 0.97em;
128+
margin-top: 2em;
129+
}
130+
131+
.hidden {
132+
display: none !important;
133+
}
134+
135+
@media (max-width: 600px) {
136+
.container {
137+
margin: 0.5em;
138+
padding: 1em 0.5em 0.5em 0.5em;
139+
}
140+
header h1 {
141+
font-size: 1.3rem;
142+
}
143+
}

verify/verify.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// MathCodeLab Certificate Verification Frontend
2+
(function() {
3+
const API_BASE = 'https://api.mathcodelab.de';
4+
const mainContent = document.getElementById('main-content');
5+
const loadingDiv = document.getElementById('loading');
6+
const resultDiv = document.getElementById('result');
7+
const searchForm = document.getElementById('search-form');
8+
const yearSpan = document.getElementById('year');
9+
if (yearSpan) yearSpan.textContent = new Date().getFullYear();
10+
11+
function getCertificateIdFromUrl() {
12+
// Support /verify/?id=... and /verify/ID
13+
const url = new URL(window.location.href);
14+
let id = url.searchParams.get('id');
15+
if (!id) {
16+
// Try to extract from path
17+
const pathParts = url.pathname.split('/').filter(Boolean);
18+
const verifyIdx = pathParts.indexOf('verify');
19+
if (verifyIdx !== -1 && pathParts.length > verifyIdx + 1) {
20+
id = pathParts[verifyIdx + 1];
21+
}
22+
}
23+
return id;
24+
}
25+
26+
function showLoading(show) {
27+
loadingDiv.classList.toggle('hidden', !show);
28+
resultDiv.innerHTML = '';
29+
searchForm.classList.add('hidden');
30+
}
31+
32+
function showSearchForm() {
33+
searchForm.classList.remove('hidden');
34+
loadingDiv.classList.add('hidden');
35+
resultDiv.innerHTML = '';
36+
}
37+
38+
function showResult(html) {
39+
loadingDiv.classList.add('hidden');
40+
searchForm.classList.add('hidden');
41+
resultDiv.innerHTML = html;
42+
}
43+
44+
function renderCertificate(data) {
45+
if (data.status === 'valid') {
46+
return `
47+
<div class="status verified">Verified Certificate</div>
48+
<dl class="certificate-details">
49+
<dt>Student Name</dt><dd>${escapeHtml(data.student_name)}</dd>
50+
<dt>Course Title</dt><dd>${escapeHtml(data.course_title)}</dd>
51+
<dt>Completion Date</dt><dd>${escapeHtml(data.completion_date)}</dd>
52+
<dt>Duration</dt><dd>${escapeHtml(data.duration_hours)} hours</dd>
53+
<dt>Issuer</dt><dd>${escapeHtml(data.issuer)}</dd>
54+
<dt>Instructor</dt><dd>${escapeHtml(data.instructor)}</dd>
55+
<dt>Certificate ID</dt><dd>${escapeHtml(data.certificate_id)}</dd>
56+
<dt>Verified At</dt><dd>${escapeHtml(data.verified_at)}</dd>
57+
</dl>
58+
<div class="note">This certificate was issued by MathCodeLab. For more information, visit <a href="https://mathcodelab.de" target="_blank">mathcodelab.de</a>.</div>
59+
`;
60+
} else if (data.status === 'revoked') {
61+
return `
62+
<div class="status revoked">Certificate Revoked</div>
63+
<dl class="certificate-details">
64+
<dt>Certificate ID</dt><dd>${escapeHtml(data.certificate_id)}</dd>
65+
${data.revocation_reason ? `<dt>Revocation Reason</dt><dd>${escapeHtml(data.revocation_reason)}</dd>` : ''}
66+
</dl>
67+
<div class="note">This certificate was revoked by MathCodeLab.</div>
68+
`;
69+
} else {
70+
return `
71+
<div class="status invalid">Certificate Not Found</div>
72+
<div class="certificate-details">
73+
<p>No certificate with this ID was found in the MathCodeLab verification system.</p>
74+
</div>
75+
`;
76+
}
77+
}
78+
79+
function escapeHtml(str) {
80+
return String(str).replace(/[&<>"']/g, function(tag) {
81+
const chars = {
82+
'&': '&amp;',
83+
'<': '&lt;',
84+
'>': '&gt;',
85+
'"': '&quot;',
86+
"'": '&#39;'
87+
};
88+
return chars[tag] || tag;
89+
});
90+
}
91+
92+
async function verifyCertificate(id) {
93+
showLoading(true);
94+
try {
95+
const resp = await fetch(`${API_BASE}/verify/${encodeURIComponent(id)}`);
96+
if (!resp.ok) throw new Error('not found');
97+
const data = await resp.json();
98+
showResult(renderCertificate(data));
99+
} catch (err) {
100+
if (err.message === 'not found') {
101+
showResult(renderCertificate({status: 'invalid', certificate_id: id}));
102+
} else {
103+
showResult('<div class="status invalid">Error</div><div class="certificate-details"><p>Could not reach the verification server. Please try again later.</p></div>');
104+
}
105+
}
106+
}
107+
108+
// Handle form submit
109+
searchForm.addEventListener('submit', function(e) {
110+
e.preventDefault();
111+
const id = document.getElementById('certificate-id').value.trim();
112+
if (id) verifyCertificate(id);
113+
});
114+
115+
// Main logic
116+
const certId = getCertificateIdFromUrl();
117+
if (certId) {
118+
verifyCertificate(certId);
119+
} else {
120+
showSearchForm();
121+
}
122+
})();

0 commit comments

Comments
 (0)