"use client"; import { useState, useTransition, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import confetti from "canvas-confetti"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import type { BangStatsDto } from "../server/queries"; import { addBang } from "../server/actions"; // ─── Celebration engine ─────────────────────────────────────────────────────── function launchFirework(originX: number, originY: number) { confetti({ particleCount: 80, startVelocity: 45, spread: 360, origin: { x: originX, y: originY }, colors: ["#ff0", "#f0f", "#0ff", "#f60", "#0f6", "#06f"], shapes: ["circle", "square"], gravity: 0.8, scalar: 1.2, ticks: 200, }); } function launchSideCannon(side: "left" | "right") { confetti({ particleCount: 120, angle: side === "left" ? 60 : 120, spread: 55, origin: { x: side === "left" ? 0 : 1, y: 0.65 }, colors: ["#ff4e50", "#fc913a", "#f9d423", "#ede574", "#e1f5c4"], shapes: ["circle", "square", "star"], scalar: 1.1, ticks: 300, }); } function launchStarBurst() { const defaults = { spread: 360, ticks: 100, gravity: 0, decay: 0.94, startVelocity: 30, shapes: ["star"] as confetti.Shape[], colors: ["FFE400", "FFBD00", "E89400", "FFCA6C", "FDFFB8"], }; function shoot() { confetti({ ...defaults, particleCount: 40, scalar: 1.2, shapes: ["star"] }); confetti({ ...defaults, particleCount: 15, scalar: 0.75, shapes: ["circle"] }); } shoot(); setTimeout(shoot, 100); setTimeout(shoot, 200); } function synthFireworkSound() { try { const ctx = new AudioContext(); // Rising "pew" tone const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.setValueAtTime(200, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(900, ctx.currentTime + 0.12); gain.gain.setValueAtTime(0.35, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.18); // Noise "boom" const bufferSize = ctx.sampleRate * 0.4; const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noise = ctx.createBufferSource(); noise.buffer = buffer; const noiseGain = ctx.createGain(); const bpFilter = ctx.createBiquadFilter(); bpFilter.type = "bandpass"; bpFilter.frequency.value = 150; noise.connect(bpFilter); bpFilter.connect(noiseGain); noiseGain.connect(ctx.destination); noiseGain.gain.setValueAtTime(0.6, ctx.currentTime + 0.15); noiseGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6); noise.start(ctx.currentTime + 0.15); noise.stop(ctx.currentTime + 0.6); } catch { // Autoplay policy blocked — silently skip } } type EmojiParticle = { id: number; emoji: string; x: number; y: number; size: number; delay: number; }; const CELEBRATION_EMOJIS = [ "💥", "🎉", "🔥", "✨", "🎊", "🥳", "💦", "😈", "🎆", "⭐", "🍆", "💫", "🌟", "🎇", "🍒", ]; function EmojiOverlay({ particles }: { particles: EmojiParticle[] }) { if (particles.length === 0) return null; return createPortal(
Last {Math.min(stats.recent.length, maxRecentBangs)}
{stats.recent.map((bang) => (No bangs yet. Add the first one!
)}