Skip to content

Commit b0de4a0

Browse files
committed
Bingo v2
1 parent 449af9c commit b0de4a0

2 files changed

Lines changed: 190 additions & 29 deletions

File tree

src/components/island/BingoCard.svelte

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@
99
if (typeof localStorage === 'undefined') return new Array(25).fill(false);
1010
try {
1111
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;
1327
} catch {}
1428
return new Array(25).fill(false);
1529
}
@@ -68,7 +82,9 @@
6882
function buildShareText() {
6983
const attended = editions.filter((_, i) => checked[i]).map(e => `${e.year} ${e.city}`);
7084
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! 🐍`;
7288
return `${base}\n${count} editions attended\n${attended.join(' · ')}`;
7389
}
7490
@@ -99,10 +115,15 @@
99115
return '';
100116
}
101117
118+
function getShareHash() {
119+
const flipped = checked.map((v, i) => v ? i : -1).filter(i => i >= 0);
120+
return flipped.join(',');
121+
}
122+
102123
let bingoCode = $derived(bingo ? getBingoCode() : '');
103124
let shareUrl = $derived(bingoCode
104125
? window.location.origin + '/bingo/' + bingoCode
105-
: window.location.origin + '/bingo');
126+
: window.location.origin + '/bingo#' + getShareHash());
106127
107128
function shareLinkedIn() {
108129
const text = buildShareText() + '\n\n' + shareUrl;
@@ -124,7 +145,7 @@
124145
window.open(`https://shareopenly.org/share/?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank', 'noopener,noreferrer');
125146
}
126147
127-
function downloadSvg() {
148+
function downloadPng() {
128149
const CELL = 124;
129150
const GAP = 6;
130151
const PAD = 20;
@@ -169,7 +190,6 @@
169190
const y = HEADER_H + row * (CELL + GAP);
170191
const flip = checked[i];
171192
172-
// card background
173193
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}"/>`;
174194
175195
if (flip) {
@@ -181,17 +201,32 @@
181201
}
182202
});
183203
184-
// watermark
185204
svg += `<text x="${W / 2}" y="${H - FOOTER_H / 2 + 8}" class="watermark">EuroPython 2026</text>`;
186205
svg += `</svg>`;
187206
207+
// Render SVG to canvas and save as PNG
188208
const blob = new Blob([svg], { type: 'image/svg+xml' });
189209
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;
195230
}
196231
</script>
197232

@@ -236,28 +271,31 @@
236271
<button class="result-close" onclick={reset} aria-label="Close and reset">✕</button>
237272
<p class="result-title">🎉 BINGO!</p>
238273
<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>
257274
<button class="result-back" onclick={reset}>← Back to game</button>
258275
</div>
259276
</div>
260277
</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>
261299
</div>
262300

263301
<style>
@@ -465,6 +503,12 @@
465503
color: var(--color-accent-themed);
466504
}
467505
506+
.share-bar {
507+
max-width: 700px;
508+
margin: -1rem auto 2.5rem;
509+
text-align: center;
510+
}
511+
468512
.share-heading {
469513
font-size: 0.75rem;
470514
font-weight: 700;

src/pages/bingo/index.astro

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
import BingoCard from "@components/island/BingoCard.svelte";
3+
---
4+
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta name="viewport" content="width=device-width, initial-scale=1" />
10+
<title>EuroPython 2026 Bingo — Play your own card!</title>
11+
<meta name="description" content="25 editions, 13 cities, one community. How many EuroPythons have you attended? Play your own bingo card!" />
12+
<meta property="og:title" content="EuroPython 2026 Bingo" />
13+
<meta property="og:description" content="25 editions, 13 cities, one community. How many EuroPythons have you attended?" />
14+
<meta property="og:site_name" content="EuroPython 2026" />
15+
<meta property="og:url" content={new URL("/bingo", Astro.url)} />
16+
<link rel="canonical" href={new URL("/bingo", Astro.url)} />
17+
<meta property="og:locale" content="en_US" />
18+
<meta name="twitter:site" content="@europython" />
19+
<meta name="twitter:card" content="summary_large_image" />
20+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
21+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
22+
<meta name="theme-color" content="#0b1121" />
23+
24+
<style is:global>
25+
:root {
26+
--color-accent: #f0c040;
27+
--color-accent-themed: #f0c040;
28+
--color-on-accent: #0b1121;
29+
--color-bg-dark: #0b1121;
30+
--color-surface-faint: #0d1520;
31+
--color-surface-subtle: rgba(255,255,255,0.05);
32+
--color-text-primary: #ffffff;
33+
--color-text-secondary: rgba(255,255,255,0.6);
34+
--color-text-muted: rgba(255,255,255,0.45);
35+
--color-text-faint: rgba(255,255,255,0.35);
36+
--color-border: rgba(255,255,255,0.12);
37+
--color-border-strong: rgba(255,255,255,0.18);
38+
}
39+
40+
* { margin: 0; padding: 0; box-sizing: border-box; }
41+
body {
42+
background: var(--color-bg-dark);
43+
min-height: 100vh;
44+
font-family: system-ui, sans-serif;
45+
display: flex;
46+
flex-direction: column;
47+
align-items: center;
48+
padding: 2rem 1rem;
49+
color: var(--color-text-primary);
50+
}
51+
52+
.bingo-page {
53+
width: 100%;
54+
max-width: 700px;
55+
}
56+
57+
.bingo-header {
58+
text-align: center;
59+
margin-bottom: 2.5rem;
60+
}
61+
62+
.bingo-title {
63+
font-size: clamp(2.5rem, 7vw, 4rem);
64+
font-weight: 800;
65+
text-transform: uppercase;
66+
letter-spacing: -0.03em;
67+
color: var(--color-accent);
68+
margin: 0 0 0.5rem;
69+
line-height: 0.95;
70+
}
71+
72+
.bingo-lead {
73+
font-size: 1.05rem;
74+
color: var(--color-text-secondary);
75+
max-width: 400px;
76+
margin: 0 auto;
77+
line-height: 1.6;
78+
}
79+
80+
.bingo-link-home {
81+
text-align: center;
82+
margin-top: 0.5rem;
83+
}
84+
85+
.bingo-link-home a {
86+
display: inline-block;
87+
color: var(--color-text-muted);
88+
font-size: 0.85rem;
89+
text-decoration: underline;
90+
text-underline-offset: 3px;
91+
transition: opacity 0.15s;
92+
}
93+
94+
.bingo-link-home a:hover {
95+
opacity: 0.7;
96+
}
97+
</style>
98+
</head>
99+
<body>
100+
<h1 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;">EuroPython 2026 Bingo</h1>
101+
102+
<div class="bingo-page">
103+
<div class="bingo-header">
104+
<div class="bingo-title">EuroPython<br />Bingo</div>
105+
<p class="bingo-lead">
106+
25 editions &middot; 13 cities &middot; One community
107+
</p>
108+
</div>
109+
110+
<BingoCard client:load />
111+
112+
<p class="bingo-link-home">
113+
<a href="/#bingo">Back to home &rarr;</a>
114+
</p>
115+
</div>
116+
</body>
117+
</html>

0 commit comments

Comments
 (0)