Skip to content

Commit c0f3a47

Browse files
authored
Merge pull request #79 from ohbm/evavanheese-certificate
Add certificate to customize
2 parents 34d0cf5 + daa0dfd commit c0f3a47

2 files changed

Lines changed: 285 additions & 0 deletions

File tree

public/certificate/certificate.png

1.42 MB
Loading

public/certificate/index.html

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
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>OHBM BrainHack 2026 | Certificate of Attendance</title>
7+
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&display=swap" rel="stylesheet" />
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
9+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
10+
<style>
11+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12+
13+
body {
14+
font-family: system-ui, sans-serif;
15+
background: #f0ece6;
16+
min-height: 100vh;
17+
display: flex;
18+
flex-direction: column;
19+
align-items: center;
20+
justify-content: flex-start;
21+
padding: 40px 20px 60px;
22+
color: #1a1a2e;
23+
}
24+
25+
h1 {
26+
font-size: 1.1rem;
27+
font-weight: 600;
28+
letter-spacing: 0.08em;
29+
text-transform: uppercase;
30+
color: #1a1a2e;
31+
margin-bottom: 6px;
32+
text-align: center;
33+
}
34+
35+
p.subtitle {
36+
font-size: 0.85rem;
37+
color: #666;
38+
text-align: center;
39+
margin-bottom: 28px;
40+
}
41+
42+
.controls {
43+
display: flex;
44+
gap: 12px;
45+
align-items: center;
46+
margin-bottom: 28px;
47+
flex-wrap: wrap;
48+
justify-content: center;
49+
}
50+
51+
input[type="text"] {
52+
border: 1.5px solid #c8c0b8;
53+
border-radius: 8px;
54+
padding: 10px 16px;
55+
font-size: 1rem;
56+
width: 280px;
57+
background: #fff;
58+
color: #1a1a2e;
59+
outline: none;
60+
transition: border-color 0.2s;
61+
}
62+
63+
input[type="text"]:focus {
64+
border-color: #fe6600;
65+
}
66+
67+
button {
68+
background: #fe6600;
69+
color: #fff;
70+
border: none;
71+
border-radius: 8px;
72+
padding: 10px 22px;
73+
font-size: 0.95rem;
74+
font-weight: 600;
75+
cursor: pointer;
76+
letter-spacing: 0.03em;
77+
transition: background 0.2s, opacity 0.2s;
78+
}
79+
80+
button:hover { background: #d95500; }
81+
button:disabled { opacity: 0.5; cursor: not-allowed; }
82+
83+
/* Certificate wrapper — maintains landscape A4 aspect ratio */
84+
.cert-wrapper {
85+
width: 100%;
86+
max-width: 860px;
87+
position: relative;
88+
}
89+
90+
/* This hidden div is what we render to PDF — fixed A4 landscape px */
91+
#cert-render {
92+
position: absolute;
93+
left: -9999px;
94+
top: 0;
95+
width: 1123px;
96+
height: 795px;
97+
}
98+
99+
#cert-render img.bg {
100+
width: 1123px;
101+
height: 795px;
102+
display: block;
103+
}
104+
105+
#cert-render .name-layer {
106+
position: absolute;
107+
top: 0; left: 0;
108+
width: 1123px;
109+
height: 795px;
110+
display: flex;
111+
align-items: center;
112+
justify-content: center;
113+
}
114+
115+
#cert-render .name-text {
116+
font-family: 'Dancing Script', cursive;
117+
font-size: 72px;
118+
color: #fe6600;
119+
margin-top: 12px;
120+
text-align: center;
121+
max-width: 760px;
122+
line-height: 1.1;
123+
}
124+
125+
/* Preview — visible scaled version */
126+
.cert-preview {
127+
width: 100%;
128+
aspect-ratio: 1123 / 795;
129+
position: relative;
130+
border-radius: 6px;
131+
overflow: hidden;
132+
box-shadow: 0 4px 24px rgba(0,0,0,0.13);
133+
}
134+
135+
.cert-preview img.bg {
136+
width: 100%;
137+
height: 100%;
138+
display: block;
139+
object-fit: cover;
140+
}
141+
142+
.cert-preview .name-overlay {
143+
position: absolute;
144+
top: 0; left: 0; right: 0; bottom: 0;
145+
display: flex;
146+
align-items: center;
147+
justify-content: center;
148+
}
149+
150+
.cert-preview .name-text-preview {
151+
font-family: 'Dancing Script', cursive;
152+
/* font-size scales with container width */
153+
font-size: 6.4vw;
154+
color: #fe6600;
155+
margin-top: 1.5%;
156+
text-align: center;
157+
max-width: 68%;
158+
line-height: 1.1;
159+
pointer-events: none;
160+
}
161+
162+
@media (min-width: 860px) {
163+
.cert-preview .name-text-preview {
164+
font-size: 55px;
165+
}
166+
}
167+
168+
.hint {
169+
font-size: 0.78rem;
170+
color: #999;
171+
text-align: center;
172+
margin-top: 14px;
173+
}
174+
175+
#status {
176+
font-size: 0.85rem;
177+
color: #fe6600;
178+
text-align: center;
179+
margin-top: 10px;
180+
min-height: 1.2em;
181+
}
182+
</style>
183+
</head>
184+
<body>
185+
186+
<h1>OHBM BrainHack 2026 | Certificate of Attendance</h1>
187+
<p class="subtitle">Enter your name as you'd like it to appear, then download your certificate.</p>
188+
189+
<div class="controls">
190+
<input
191+
type="text"
192+
id="nameInput"
193+
placeholder="Your full name"
194+
maxlength="60"
195+
autocomplete="name"
196+
/>
197+
<button id="dlBtn" disabled>Download PDF</button>
198+
</div>
199+
200+
<!-- Visible preview -->
201+
<div class="cert-wrapper">
202+
<div class="cert-preview">
203+
<img class="bg" src="certificate.png" alt="Certificate background" id="bgPreview" />
204+
<div class="name-overlay">
205+
<div class="name-text-preview" id="namePreview">&nbsp;</div>
206+
</div>
207+
</div>
208+
</div>
209+
210+
<!-- Hidden high-res render target -->
211+
<div id="cert-render" aria-hidden="true">
212+
<img class="bg" src="certificate.png" alt="" id="bgRender" crossorigin="anonymous" />
213+
<div class="name-layer">
214+
<div class="name-text" id="nameRender">&nbsp;</div>
215+
</div>
216+
</div>
217+
218+
<p id="status"></p>
219+
<p class="hint">Your name is only used locally in your browser — nothing is sent to any server.</p>
220+
221+
<script>
222+
const input = document.getElementById('nameInput');
223+
const preview = document.getElementById('namePreview');
224+
const render = document.getElementById('nameRender');
225+
const dlBtn = document.getElementById('dlBtn');
226+
const status = document.getElementById('status');
227+
228+
input.addEventListener('input', () => {
229+
const val = input.value.trim();
230+
preview.textContent = val || '\u00a0';
231+
render.textContent = val || '\u00a0';
232+
dlBtn.disabled = val.length === 0;
233+
});
234+
235+
dlBtn.addEventListener('click', async () => {
236+
const name = input.value.trim();
237+
if (!name) return;
238+
239+
dlBtn.disabled = true;
240+
status.textContent = 'Generating your certificate…';
241+
242+
try {
243+
// Make sure the font is loaded
244+
await document.fonts.ready;
245+
246+
const { jsPDF } = window.jspdf;
247+
248+
const certEl = document.getElementById('cert-render');
249+
certEl.style.left = '0';
250+
certEl.style.position = 'fixed';
251+
certEl.style.top = '-9999px';
252+
253+
const canvas = await html2canvas(certEl, {
254+
scale: 2,
255+
useCORS: true,
256+
allowTaint: false,
257+
backgroundColor: null,
258+
width: 1123,
259+
height: 795,
260+
logging: false,
261+
});
262+
263+
certEl.style.left = '-9999px';
264+
certEl.style.position = 'absolute';
265+
266+
const imgData = canvas.toDataURL('image/jpeg', 0.97);
267+
268+
// A4 landscape in mm: 297 x 210
269+
const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
270+
pdf.addImage(imgData, 'JPEG', 0, 0, 297, 210);
271+
272+
const safeName = name.replace(/[^a-z0-9]/gi, '_');
273+
pdf.save(`OHBM_BrainHack_2026_Certificate_${safeName}.pdf`);
274+
275+
status.textContent = 'Certificate downloaded!';
276+
} catch (err) {
277+
console.error(err);
278+
status.textContent = 'Something went wrong. Please try again.';
279+
} finally {
280+
dlBtn.disabled = false;
281+
}
282+
});
283+
</script>
284+
</body>
285+
</html>

0 commit comments

Comments
 (0)