Skip to content

Commit 2901be5

Browse files
committed
add confetti popup draft
1 parent f13d353 commit 2901be5

4 files changed

Lines changed: 250 additions & 1 deletion

File tree

assets/css/custom.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,14 @@ textarea:read-only {
197197
}
198198

199199
/* offsetting anchor targets to adjust for fixed navbar */
200-
[id]::before {
200+
[id]:not(#startup-popup-countdown)::before {
201201
content: '';
202202
display: block;
203203
height: 96px;
204204
margin-top: -96px;
205205
visibility: hidden;
206206
}
207+
208+
body.popup-open {
209+
overflow: hidden;
210+
}

assets/js/base.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,201 @@ function formatNumber(num, locale) {
6060
return formatted;
6161
}
6262
}
63+
64+
class StartupPopupConfetti {
65+
constructor(canvas) {
66+
this.canvas = canvas;
67+
this.context = canvas.getContext('2d');
68+
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
69+
this.animationFrameId = null;
70+
this.confettiPieces = [];
71+
this.viewportWidth = window.innerWidth;
72+
this.viewportHeight = window.innerHeight;
73+
this.confettiColors = ['#ef4444', '#dc2626', '#f97316', '#f59e0b', '#fde047', '#3b82f6', '#2563eb', '#60a5fa'];
74+
this.confettiCount = Math.min(Math.max(Math.floor(this.viewportWidth / 6), 80), 220);
75+
this.handleResize = this.resizeCanvas.bind(this);
76+
this.renderFrame = this.renderFrame.bind(this);
77+
}
78+
79+
randomBetween(min, max) {
80+
return Math.random() * (max - min) + min;
81+
}
82+
83+
createConfettiPiece(withinViewport) {
84+
return {
85+
x: this.randomBetween(0, this.viewportWidth),
86+
y: withinViewport ? this.randomBetween(-this.viewportHeight, this.viewportHeight) : this.randomBetween(-this.viewportHeight * 0.3, -20),
87+
size: this.randomBetween(6, 13),
88+
speedY: this.randomBetween(1.2, 3.6),
89+
drift: this.randomBetween(-1.2, 1.2),
90+
rotation: this.randomBetween(0, Math.PI * 2),
91+
rotationSpeed: this.randomBetween(-0.08, 0.08),
92+
color: this.confettiColors[Math.floor(Math.random() * this.confettiColors.length)],
93+
};
94+
}
95+
96+
resizeCanvas() {
97+
this.viewportWidth = window.innerWidth;
98+
this.viewportHeight = window.innerHeight;
99+
const dpr = window.devicePixelRatio || 1;
100+
this.canvas.width = Math.floor(this.viewportWidth * dpr);
101+
this.canvas.height = Math.floor(this.viewportHeight * dpr);
102+
this.canvas.style.width = `${this.viewportWidth}px`;
103+
this.canvas.style.height = `${this.viewportHeight}px`;
104+
if (this.context) {
105+
this.context.setTransform(dpr, 0, 0, dpr, 0, 0);
106+
}
107+
}
108+
109+
renderFrame() {
110+
if (!this.context) {
111+
return;
112+
}
113+
this.context.clearRect(0, 0, this.viewportWidth, this.viewportHeight);
114+
115+
for (let i = 0; i < this.confettiPieces.length; i += 1) {
116+
const piece = this.confettiPieces[i];
117+
piece.y += piece.speedY;
118+
piece.x += piece.drift + Math.sin((piece.y + piece.rotation) * 0.01) * 0.7;
119+
piece.rotation += piece.rotationSpeed;
120+
121+
if (piece.y > this.viewportHeight + 24 || piece.x < -24 || piece.x > this.viewportWidth + 24) {
122+
this.confettiPieces[i] = this.createConfettiPiece(false);
123+
}
124+
}
125+
126+
for (const piece of this.confettiPieces) {
127+
this.context.save();
128+
this.context.translate(piece.x, piece.y);
129+
this.context.rotate(piece.rotation);
130+
this.context.fillStyle = piece.color;
131+
this.context.fillRect(-piece.size / 2, -piece.size / 2, piece.size, piece.size * 0.6);
132+
this.context.restore();
133+
}
134+
135+
this.animationFrameId = window.requestAnimationFrame(this.renderFrame);
136+
}
137+
138+
start() {
139+
if (!this.context) {
140+
return;
141+
}
142+
this.resizeCanvas();
143+
this.confettiPieces = Array.from({ length: this.confettiCount }, () => this.createConfettiPiece(true));
144+
if (!this.prefersReducedMotion) {
145+
this.renderFrame();
146+
}
147+
window.addEventListener('resize', this.handleResize);
148+
}
149+
150+
stop() {
151+
if (this.animationFrameId) {
152+
window.cancelAnimationFrame(this.animationFrameId);
153+
this.animationFrameId = null;
154+
}
155+
window.removeEventListener('resize', this.handleResize);
156+
}
157+
}
158+
159+
const STARTUP_POPUP_CONFIG = {
160+
enabled: true,
161+
showOnlyOnce: false,
162+
seenStorageKey: 'cryptomator-startup-popup-seen',
163+
};
164+
165+
(function setupStartupPopup() {
166+
const popup = document.getElementById('startup-popup');
167+
if (!popup || !STARTUP_POPUP_CONFIG.enabled) {
168+
return;
169+
}
170+
171+
if (STARTUP_POPUP_CONFIG.showOnlyOnce) {
172+
try {
173+
const hasSeenPopup = window.localStorage.getItem(STARTUP_POPUP_CONFIG.seenStorageKey) === 'true';
174+
if (hasSeenPopup) {
175+
return;
176+
}
177+
window.localStorage.setItem(STARTUP_POPUP_CONFIG.seenStorageKey, 'true');
178+
} catch {
179+
// localStorage can be unavailable in private browsing modes
180+
}
181+
}
182+
183+
popup.classList.remove('hidden');
184+
popup.removeAttribute('aria-hidden');
185+
document.body.classList.add('popup-open');
186+
187+
const closeButton = document.getElementById('startup-popup-close');
188+
const canvas = document.getElementById('startup-popup-confetti');
189+
const countdown = document.getElementById('startup-popup-countdown');
190+
191+
if (countdown) {
192+
const targetTime = new Date(2026, 2, 9, 0, 0, 0, 0).getTime();
193+
let countdownIntervalId = null;
194+
const daysElement = countdown.querySelector('[data-unit="days"]');
195+
const hoursElement = countdown.querySelector('[data-unit="hours"]');
196+
const minutesElement = countdown.querySelector('[data-unit="minutes"]');
197+
const secondsElement = countdown.querySelector('[data-unit="seconds"]');
198+
199+
function formatTime(value) {
200+
return String(value).padStart(2, '0');
201+
}
202+
203+
function setCountdownValue(days, hours, minutes, seconds) {
204+
if (daysElement && hoursElement && minutesElement && secondsElement) {
205+
daysElement.textContent = formatTime(days);
206+
hoursElement.textContent = formatTime(hours);
207+
minutesElement.textContent = formatTime(minutes);
208+
secondsElement.textContent = formatTime(seconds);
209+
return;
210+
}
211+
212+
countdown.textContent = `${formatTime(days)}:${formatTime(hours)}:${formatTime(minutes)}:${formatTime(seconds)}`;
213+
}
214+
215+
function updateCountdown() {
216+
const remaining = targetTime - Date.now();
217+
if (remaining <= 0) {
218+
setCountdownValue(0, 0, 0, 0);
219+
if (countdownIntervalId) {
220+
window.clearInterval(countdownIntervalId);
221+
}
222+
return;
223+
}
224+
225+
const days = Math.floor(remaining / (24 * 60 * 60 * 1000));
226+
const hours = Math.floor((remaining % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
227+
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
228+
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
229+
setCountdownValue(days, hours, minutes, seconds);
230+
}
231+
232+
updateCountdown();
233+
countdownIntervalId = window.setInterval(updateCountdown, 1000);
234+
popup.addEventListener('popup:close', () => {
235+
if (countdownIntervalId) {
236+
window.clearInterval(countdownIntervalId);
237+
}
238+
}, { once: true });
239+
}
240+
241+
if (canvas) {
242+
const confetti = new StartupPopupConfetti(canvas);
243+
confetti.start();
244+
245+
popup.addEventListener('popup:close', () => {
246+
confetti.stop();
247+
}, { once: true });
248+
}
249+
250+
if (!closeButton) {
251+
return;
252+
}
253+
254+
closeButton.addEventListener('click', () => {
255+
popup.classList.add('hidden');
256+
popup.setAttribute('aria-hidden', 'true');
257+
popup.dispatchEvent(new CustomEvent('popup:close'));
258+
document.body.classList.remove('popup-open');
259+
}, { once: true });
260+
})();

layouts/_default/baseof.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,53 @@
6464
{{ end }}
6565
</head>
6666
<body x-data="{ isNavOpen: false, globalData: { githubStargazers: null, mastodonFollowers: null } }" x-init="determineGlobalData('{{ .Site.Language.Lang }}', globalData)" :class="isNavOpen && 'overflow-hidden'" class="bg-dark font-body text-gray-900">
67+
<div id="startup-popup" class="hidden fixed inset-0 bg-green-500 z-[9999]" role="dialog" aria-modal="true" aria-hidden="true">
68+
<canvas id="startup-popup-confetti" class="absolute inset-0 w-full h-full pointer-events-none" aria-hidden="true"></canvas>
69+
<button id="startup-popup-close" type="button" class="absolute top-4 right-4 z-20 text-white text-4xl leading-none cursor-pointer p-1" aria-label="Close popup">
70+
&times;
71+
</button>
72+
<div class="relative z-10 flex h-full flex-col px-6 py-10 md:px-12 md:py-14">
73+
<div class="flex-1 flex flex-col justify-center">
74+
<h2 class="font-headline font-bold text-3xl md:text-5xl text-white text-center">The vault opens in...</h2>
75+
<div class="w-full max-w-5xl mx-auto flex flex-col md:flex-row md:items-center md:justify-center gap-8 md:gap-12">
76+
<div class="flex justify-center">
77+
<img src="/img/cryptobot-party.png" alt="Cryptobot Party" class="w-40 md:w-56 h-auto">
78+
</div>
79+
<div class="text-center md:text-left">
80+
<div id="startup-popup-countdown" class="grid grid-cols-4 gap-3 sm:gap-4 w-fit mx-auto md:mx-0" aria-live="polite">
81+
<div class="flex flex-col items-center">
82+
<span data-unit="days" class="font-headline font-bold text-white text-3xl md:text-6xl tracking-wide">00</span>
83+
<span class="mt-2 text-white/90 text-[10px] md:text-sm uppercase whitespace-nowrap tracking-normal md:tracking-wide">Days</span>
84+
</div>
85+
<div class="flex flex-col items-center">
86+
<span data-unit="hours" class="font-headline font-bold text-white text-3xl md:text-6xl tracking-wide">00</span>
87+
<span class="mt-2 text-white/90 text-[10px] md:text-sm uppercase whitespace-nowrap tracking-normal md:tracking-wide">Hours</span>
88+
</div>
89+
<div class="flex flex-col items-center">
90+
<span data-unit="minutes" class="font-headline font-bold text-white text-3xl md:text-6xl tracking-wide">00</span>
91+
<span class="mt-2 text-white/90 text-[10px] md:text-sm uppercase whitespace-nowrap tracking-normal md:tracking-wide">Minutes</span>
92+
</div>
93+
<div class="flex flex-col items-center">
94+
<span data-unit="seconds" class="font-headline font-bold text-white text-3xl md:text-6xl tracking-wide">00</span>
95+
<span class="mt-2 text-white/90 text-[10px] md:text-sm uppercase whitespace-nowrap tracking-normal md:tracking-wide">Seconds</span>
96+
</div>
97+
</div>
98+
<p class="text-white text-base md:text-lg mt-3 text-center">Something special is unlucking soon!</p>
99+
</div>
100+
</div>
101+
</div>
102+
<div class="pt-5">
103+
<div class="mt-1 flex items-center justify-center gap-6">
104+
<a class="text-white/90 hover:text-white no-underline hover:no-underline" href="https://mastodon.online/@cryptomator" aria-label="Cryptomator on Mastodon" target="_blank" rel="noopener me" data-umami-event="popup-mastodon">
105+
<i class="fa-brands fa-mastodon text-2xl"></i>
106+
</a>
107+
<a class="text-white/90 hover:text-white no-underline hover:no-underline" href="https://www.linkedin.com/company/skymatic/" aria-label="Skymatic on LinkedIn" target="_blank" rel="noopener" data-umami-event="popup-linkedin">
108+
<i class="fa-brands fa-linkedin text-2xl"></i>
109+
</a>
110+
</div>
111+
</div>
112+
</div>
113+
</div>
67114
{{ partial "nav.html" . }}
68115
{{- $topPadding := cond .IsHome "" "pt-12" -}}
69116
<div class="bg-gray-100 min-h-[80vh] {{ $topPadding }}">

static/img/cryptobot-party.png

96.9 KB
Loading

0 commit comments

Comments
 (0)