|
9 | 9 | if (typeof localStorage === 'undefined') return new Array(25).fill(false); |
10 | 10 | try { |
11 | 11 | const saved = localStorage.getItem(STORAGE_KEY); |
12 | | - if (saved) return JSON.parse(saved); |
| 12 | + let arr = saved ? JSON.parse(saved) : new Array(25).fill(false); |
| 13 | +
|
| 14 | + // Restore card state from URL hash (e.g. /bingo#0,5,12,17) |
| 15 | + if (typeof window !== 'undefined' && window.location.hash.length > 1) { |
| 16 | + const indices = window.location.hash |
| 17 | + .slice(1) |
| 18 | + .split(',') |
| 19 | + .map(Number) |
| 20 | + .filter((n) => !isNaN(n) && n >= 0 && n < 25); |
| 21 | + indices.forEach((i) => { |
| 22 | + arr[i] = true; |
| 23 | + }); |
| 24 | + } |
| 25 | +
|
| 26 | + return arr; |
13 | 27 | } catch {} |
14 | 28 | return new Array(25).fill(false); |
15 | 29 | } |
|
68 | 82 | function buildShareText() { |
69 | 83 | const attended = editions.filter((_, i) => checked[i]).map(e => `${e.year} ${e.city}`); |
70 | 84 | const count = attended.length; |
71 | | - const base = `I just completed #EuroPythonBingo! 🐍`; |
| 85 | + const base = bingo |
| 86 | + ? `I just completed #EuroPythonBingo! 🐍` |
| 87 | + : `I'm playing #EuroPythonBingo! 🐍`; |
72 | 88 | return `${base}\n${count} editions attended\n${attended.join(' · ')}`; |
73 | 89 | } |
74 | 90 |
|
|
99 | 115 | return ''; |
100 | 116 | } |
101 | 117 |
|
| 118 | + function getShareHash() { |
| 119 | + const flipped = checked.map((v, i) => v ? i : -1).filter(i => i >= 0); |
| 120 | + return flipped.join(','); |
| 121 | + } |
| 122 | +
|
102 | 123 | let bingoCode = $derived(bingo ? getBingoCode() : ''); |
103 | 124 | let shareUrl = $derived(bingoCode |
104 | 125 | ? window.location.origin + '/bingo/' + bingoCode |
105 | | - : window.location.origin + '/bingo'); |
| 126 | + : window.location.origin + '/bingo#' + getShareHash()); |
106 | 127 |
|
107 | 128 | function shareLinkedIn() { |
108 | 129 | const text = buildShareText() + '\n\n' + shareUrl; |
|
124 | 145 | window.open(`https://shareopenly.org/share/?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank', 'noopener,noreferrer'); |
125 | 146 | } |
126 | 147 |
|
127 | | - function downloadSvg() { |
| 148 | + function downloadPng() { |
128 | 149 | const CELL = 124; |
129 | 150 | const GAP = 6; |
130 | 151 | const PAD = 20; |
|
169 | 190 | const y = HEADER_H + row * (CELL + GAP); |
170 | 191 | const flip = checked[i]; |
171 | 192 |
|
172 | | - // card background |
173 | 193 | svg += `<rect x="${x}" y="${y}" width="${CELL}" height="${CELL}" rx="3" fill="${flip ? flippedBg : cellBg}" stroke="${flip ? accentColor : cellBorder}" stroke-width="${flip ? 2 : 1}"/>`; |
174 | 194 |
|
175 | 195 | if (flip) { |
|
181 | 201 | } |
182 | 202 | }); |
183 | 203 |
|
184 | | - // watermark |
185 | 204 | svg += `<text x="${W / 2}" y="${H - FOOTER_H / 2 + 8}" class="watermark">EuroPython 2026</text>`; |
186 | 205 | svg += `</svg>`; |
187 | 206 |
|
| 207 | + // Render SVG to canvas and save as PNG |
188 | 208 | const blob = new Blob([svg], { type: 'image/svg+xml' }); |
189 | 209 | const url = URL.createObjectURL(blob); |
190 | | - const a = document.createElement('a'); |
191 | | - a.href = url; |
192 | | - a.download = 'europython-bingo.svg'; |
193 | | - a.click(); |
194 | | - URL.revokeObjectURL(url); |
| 210 | + const img = new Image(); |
| 211 | + img.onload = () => { |
| 212 | + const canvas = document.createElement('canvas'); |
| 213 | + canvas.width = W; |
| 214 | + canvas.height = H; |
| 215 | + const ctx = canvas.getContext('2d'); |
| 216 | + ctx.drawImage(img, 0, 0); |
| 217 | + URL.revokeObjectURL(url); |
| 218 | + canvas.toBlob((pngBlob) => { |
| 219 | + const a = document.createElement('a'); |
| 220 | + a.href = URL.createObjectURL(pngBlob); |
| 221 | + a.download = 'europython-bingo.png'; |
| 222 | + a.click(); |
| 223 | + }); |
| 224 | + }; |
| 225 | + img.onerror = () => { |
| 226 | + URL.revokeObjectURL(url); |
| 227 | + console.error('Failed to render bingo PNG'); |
| 228 | + }; |
| 229 | + img.src = url; |
195 | 230 | } |
196 | 231 | </script> |
197 | 232 |
|
|
236 | 271 | <button class="result-close" onclick={reset} aria-label="Close and reset">✕</button> |
237 | 272 | <p class="result-title">🎉 BINGO!</p> |
238 | 273 | <p class="result-sub">You completed a line!</p> |
239 | | - <p class="share-heading">Share your card</p> |
240 | | - <div class="share-row"> |
241 | | - <button class="share-btn" onclick={shareLinkedIn} aria-label="Share on LinkedIn"> |
242 | | - <svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg> |
243 | | - </button> |
244 | | - <button class="share-btn" onclick={shareX} aria-label="Share on X"> |
245 | | - <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> |
246 | | - </button> |
247 | | - <button class="share-btn" onclick={shareBlueSky} aria-label="Share on BlueSky"> |
248 | | - <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.204-.659-.3-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/></svg> |
249 | | - </button> |
250 | | - <button class="share-btn" onclick={shareMastodon} aria-label="Share on Mastodon"> |
251 | | - <svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg> |
252 | | - </button> |
253 | | - <button class="share-btn share-btn--save" onclick={downloadSvg} aria-label="Download SVG"> |
254 | | - Download SVG |
255 | | - </button> |
256 | | - </div> |
257 | 274 | <button class="result-back" onclick={reset}>← Back to game</button> |
258 | 275 | </div> |
259 | 276 | </div> |
260 | 277 | </div> |
| 278 | + |
| 279 | + <div class="share-bar"> |
| 280 | + <p class="share-heading">Share your card</p> |
| 281 | + <div class="share-row"> |
| 282 | + <button class="share-btn" onclick={shareLinkedIn} aria-label="Share on LinkedIn"> |
| 283 | + <svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg> |
| 284 | + </button> |
| 285 | + <button class="share-btn" onclick={shareX} aria-label="Share on X"> |
| 286 | + <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> |
| 287 | + </button> |
| 288 | + <button class="share-btn" onclick={shareBlueSky} aria-label="Share on BlueSky"> |
| 289 | + <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.204-.659-.3-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/></svg> |
| 290 | + </button> |
| 291 | + <button class="share-btn" onclick={shareMastodon} aria-label="Share on Mastodon"> |
| 292 | + <svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg> |
| 293 | + </button> |
| 294 | + <button class="share-btn share-btn--save" onclick={downloadPng} aria-label="Download PNG"> |
| 295 | + Download PNG |
| 296 | + </button> |
| 297 | + </div> |
| 298 | + </div> |
261 | 299 | </div> |
262 | 300 |
|
263 | 301 | <style> |
|
465 | 503 | color: var(--color-accent-themed); |
466 | 504 | } |
467 | 505 |
|
| 506 | + .share-bar { |
| 507 | + max-width: 700px; |
| 508 | + margin: -1rem auto 2.5rem; |
| 509 | + text-align: center; |
| 510 | + } |
| 511 | +
|
468 | 512 | .share-heading { |
469 | 513 | font-size: 0.75rem; |
470 | 514 | font-weight: 700; |
|
0 commit comments