Skip to content

Commit bbbafc5

Browse files
authored
Merge pull request #57 from Azure-Samples/copilot/add-confeti-visual-effect
Add participant assignment confetti celebration on details open
2 parents 5ebbbe6 + 2178701 commit bbbafc5

1 file changed

Lines changed: 55 additions & 1 deletion

File tree

‎src/components/AssignmentView.tsx‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback } from 'react'
1+
import { useState, useEffect, useCallback, useRef } from 'react'
22
import { Button } from '@/components/ui/button'
33
import { Card } from '@/components/ui/card'
44
import { Badge } from '@/components/ui/badge'
@@ -53,6 +53,21 @@ interface AssignmentViewProps {
5353
emailConfigured?: boolean
5454
}
5555

56+
const CONFETTI_COLORS = ['#F43F5E', '#F59E0B', '#10B981', '#3B82F6', '#A855F7', '#EC4899']
57+
// Using a prime factor avoids repeating columns too early and helps spread 24 pieces across the viewport.
58+
const CONFETTI_HORIZONTAL_SPACING_FACTOR = 11
59+
const CONFETTI_PIECES = Array.from({ length: 24 }, (_, index) => ({
60+
id: index,
61+
color: CONFETTI_COLORS[index % CONFETTI_COLORS.length],
62+
left: `${(index * CONFETTI_HORIZONTAL_SPACING_FACTOR) % 100}%`,
63+
delay: (index % 6) * 0.08,
64+
duration: 1.8 + (index % 5) * 0.2,
65+
drift: ((index % 7) - 3) * 14
66+
}))
67+
const CONFETTI_DISPLAY_DURATION_MS = (
68+
Math.max(...CONFETTI_PIECES.map((piece) => piece.delay + piece.duration)) * 1000
69+
)
70+
5671
export function AssignmentView({
5772
game,
5873
participant,
@@ -75,6 +90,8 @@ export function AssignmentView({
7590
const [isConfirming, setIsConfirming] = useState(false)
7691
const [isRefreshing, setIsRefreshing] = useState(false)
7792
const [giverHasConfirmed, setGiverHasConfirmed] = useState(false)
93+
const [showConfetti, setShowConfetti] = useState(false)
94+
const lastConfettiReceiverId = useRef<string | null>(null)
7895

7996
// Refresh game data from API
8097
const refreshGameData = useCallback(async () => {
@@ -142,6 +159,15 @@ export function AssignmentView({
142159
return () => clearTimeout(timer)
143160
}, [currentReceiver])
144161

162+
useEffect(() => {
163+
if (!currentReceiver || lastConfettiReceiverId.current === currentReceiver.id) return
164+
165+
lastConfettiReceiverId.current = currentReceiver.id
166+
setShowConfetti(true)
167+
const timer = setTimeout(() => setShowConfetti(false), CONFETTI_DISPLAY_DURATION_MS)
168+
return () => clearTimeout(timer)
169+
}, [currentReceiver])
170+
145171
// Note: No mount-time refresh needed - game data is already loaded when entering this view
146172
// refreshGameData is available for manual refresh via the refresh button only
147173

@@ -454,6 +480,34 @@ export function AssignmentView({
454480

455481
return (
456482
<div className="min-h-screen bg-background">
483+
{showConfetti && (
484+
<div
485+
aria-hidden="true"
486+
data-testid="assignment-confetti-overlay"
487+
className="fixed inset-0 pointer-events-none overflow-hidden z-50"
488+
>
489+
{CONFETTI_PIECES.map((piece) => (
490+
<motion.span
491+
key={piece.id}
492+
className="absolute top-[-12vh] h-3 w-2 rounded-sm"
493+
style={{ left: piece.left, backgroundColor: piece.color }}
494+
initial={{ opacity: 0, y: '-12vh', rotate: 0 }}
495+
animate={{
496+
opacity: [0, 1, 1, 0],
497+
y: '112vh',
498+
x: [0, piece.drift, piece.drift / 2],
499+
rotate: [0, 280]
500+
}}
501+
transition={{
502+
duration: piece.duration,
503+
delay: piece.delay,
504+
ease: 'easeOut'
505+
}}
506+
/>
507+
))}
508+
</div>
509+
)}
510+
457511
<header className="flex justify-between items-center p-4 border-b">
458512
<Button variant="ghost" onClick={onBack} className="gap-2">
459513
<ArrowLeft size={20} />

0 commit comments

Comments
 (0)