From 4db2a343c2bea49fd159b7e3508059ed9d0c6e59 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Tue, 3 Feb 2026 01:32:06 +0100 Subject: [PATCH] Add confetti burst after suggestion submit --- wwwroot/app.js | 130 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 9 deletions(-) diff --git a/wwwroot/app.js b/wwwroot/app.js index 36e66fb..cd018f7 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -507,15 +507,16 @@ function setupHandlers() { if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) { return toast(t("toast.invalidImageUrl"), true); } - try { - await api.createSuggestion(data); - form.reset(); - toast(t("toast.suggestionAdded")); - await loadSuggestData(); - } catch (err) { - toast(err.message, true); - } - }); + try { + await api.createSuggestion(data); + form.reset(); + toast(t("toast.suggestionAdded")); + triggerCelebration(form.querySelector("button[type=submit]")); + await loadSuggestData(); + } catch (err) { + toast(err.message, true); + } +}); $("set-phase").addEventListener("click", async () => { const phase = $("phase-select").value; @@ -874,6 +875,117 @@ function setupBackgroundPan(config = {}) { step(); } +// Celebration FX ----------------------------------------------------- +let fxCanvas; +let fxCtx; +let fxParticles = []; +let fxAnimating = false; + +function ensureFxCanvas() { + if (fxCanvas) return; + fxCanvas = document.createElement("canvas"); + fxCanvas.className = "fx-canvas"; + fxCanvas.style.position = "fixed"; + fxCanvas.style.inset = "0"; + fxCanvas.style.pointerEvents = "none"; + fxCanvas.style.zIndex = "120"; + fxCanvas.width = window.innerWidth; + fxCanvas.height = window.innerHeight; + fxCtx = fxCanvas.getContext("2d"); + document.body.appendChild(fxCanvas); + window.addEventListener("resize", () => { + fxCanvas.width = window.innerWidth; + fxCanvas.height = window.innerHeight; + }); +} + +function triggerCelebration(button) { + ensureFxCanvas(); + const rect = (button || document.body).getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + spawnConfetti(x, y, 80); + spawnFirework(x, y, 40); + if (!fxAnimating) { + fxAnimating = true; + requestAnimationFrame(fxStep); + } +} + +function spawnConfetti(x, y, count) { + for (let i = 0; i < count; i++) { + fxParticles.push({ + x, + y, + vx: (Math.random() - 0.5) * 6, + vy: Math.random() * -6 - 2, + size: 6 + Math.random() * 4, + life: 60 + Math.random() * 20, + color: randomColor(), + type: "confetti", + wobble: Math.random() * Math.PI * 2, + }); + } +} + +function spawnFirework(x, y, count) { + for (let i = 0; i < count; i++) { + const angle = (Math.PI * 2 * i) / count + Math.random() * 0.3; + const speed = 3 + Math.random() * 3; + fxParticles.push({ + x, + y, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + size: 4 + Math.random() * 3, + life: 50 + Math.random() * 20, + color: randomColor(), + type: "spark", + }); + } +} + +function fxStep() { + if (!fxCtx || !fxCanvas) return; + fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); + fxParticles = fxParticles.filter((p) => p.life > 0); + for (const p of fxParticles) { + if (p.type === "confetti") { + p.vy += 0.18; + p.vx *= 0.99; + p.wobble += 0.2; + p.x += p.vx + Math.cos(p.wobble) * 0.8; + p.y += p.vy; + } else { + p.vy += 0.08; + p.vx *= 0.995; + p.x += p.vx; + p.y += p.vy; + } + p.life -= 1; + fxCtx.fillStyle = p.color; + fxCtx.beginPath(); + if (p.type === "confetti") { + fxCtx.fillRect(p.x, p.y, p.size, p.size * 0.6); + } else { + fxCtx.arc(p.x, p.y, p.size * 0.5, 0, Math.PI * 2); + fxCtx.fill(); + } + } + if (fxParticles.length > 0) { + requestAnimationFrame(fxStep); + } else { + fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); + fxAnimating = false; + } +} + +function randomColor() { + const palette = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"]; + return palette[Math.floor(Math.random() * palette.length)]; +} +// ------------------------------------------------------------------- + async function main() { setupHandlers(); try {