feat: add confetti fireworks effect on tour completion

This commit is contained in:
Shine1i 2026-02-15 20:25:23 +01:00
commit def787e6d4
2 changed files with 61 additions and 0 deletions

View file

@ -18,6 +18,7 @@ import {
useState,
} from "react";
import { cssEscape, toRect } from "../lib/dom";
import { fireConfettiFireworks } from "../lib/confetti-fireworks";
import { computeCardPos, padded, pickPlacement } from "../lib/layout";
import type { Placement, Rect, TourStep } from "../types";
@ -230,6 +231,7 @@ export function GuidedTour({
function requestClose(reason: "skip" | "complete") {
if (closeLockRef.current) return;
closeLockRef.current = true;
void fireConfettiFireworks();
if (reason === "skip") onSkip();
else onComplete();
onOpenChange(false);

View file

@ -0,0 +1,59 @@
"use client";
type FireworksOpts = {
durationMs?: number;
intervalMs?: number;
zIndex?: number;
};
export async function fireConfettiFireworks(opts: FireworksOpts = {}) {
try {
if (typeof window === "undefined") return;
const prefersReduce =
window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
if (prefersReduce) return;
const confetti = (await import("canvas-confetti")).default;
const duration = opts.durationMs ?? 1200;
const intervalMs = opts.intervalMs ?? 240;
const animationEnd = Date.now() + duration;
const defaults = {
startVelocity: 28,
spread: 360,
ticks: 58,
zIndex: opts.zIndex ?? 99999,
disableForReducedMotion: true,
} as const;
const randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min;
const interval = window.setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
window.clearInterval(interval);
return;
}
const particleCount = Math.max(
10,
Math.floor(36 * (timeLeft / duration)),
);
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.12, 0.3), y: Math.random() - 0.2 },
});
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.7, 0.88), y: Math.random() - 0.2 },
});
}, intervalMs);
} catch {
// best-effort
}
}