({ ...m, selected: null })), noPrizes: @js($prizes->isEmpty()), noParticipants: @js(collect($participants)->isEmpty()), participantMismatch: false, activePrizeIndex: -1, prizeInterval: null, drawId: @js($draw->id), users: @js(collect($participants)->map(fn($p) => ['id' => $p['id'], 'full_name' => $p['full_name'], 'email' => $p['email'], 'display_email' => $p['display_email']])->values() ?? []), prizes: @js($prizes->values() ?? []), winners: [], spinning: [], winnersByPrize: [], serverWinners: @js($serverWinners ?? []), mediaRecorder: null, recordedChunks: [], screenStream: null, isRecording: false, drawStarted: false, completedPrizes: [], recordingCancelled: false, currentPrizeIndex: 0, isRevealing: false, saving: false, init() { //this.resetDraw(); this.spinning = Array(this.prizes.length).fill(false); this.winners = Array(this.prizes.length).fill(null); console.log(this.winners); if (!this.noParticipants && this.prizes.length) { const totalExpectedWinners = this.prizes.reduce((sum, p) => sum + (p.winner_count || 1), 0); if (this.users.length < totalExpectedWinners) { this.participantMismatch = true; } } }, easeOut(t) { return 1 - Math.pow(1 - t, 3); }, getServerWinner(prizeIndex, winnerIndex) { const prize = this.prizes[prizeIndex]; if (!prize) return null; const prizeId = prize.id; const winnersForPrize = this.serverWinners[prizeId] || []; return winnersForPrize[winnerIndex] || null; }, pickWinner(boxIndex) { const usedIds = this.winners.filter(Boolean).map(w => w.id); const avail = this.users.filter(u => !usedIds.includes(u.id)); if (!avail.length) return this.users[Math.floor(Math.random() * this.users.length)]; return avail[Math.floor(Math.random() * avail.length)]; }, async saveWinnersToDB() { try { //alert('winner'); await this.$wire.saveWinners(this.serverWinners); } catch (error) { console.error('Error saving winners:', error); } }, async animateSingleWinner(boxEl, prizeIndex, winnerIndex) { const list = boxEl.querySelector('.list'); this.resetSpinner(boxEl); let winner = this.getServerWinner(prizeIndex, winnerIndex); if (!winner) return null; const users = this.users; const winnerIndexInUsers = users.findIndex(u => u.id === winner.id); if (winnerIndexInUsers === -1) return null; const ITEM_HEIGHT = 44; const CENTER_OFFSET = 2; // smoother long reel const cycles = 20; const targetIndex = (cycles * users.length) - (users.length - winnerIndexInUsers) - CENTER_OFFSET; const finalPos = targetIndex * ITEM_HEIGHT; const duration = 4500; let start = null; // sound control let lastTick = -1; return new Promise(resolve => { const step = (ts) => { if (!start) start = ts; const progress = Math.min((ts - start) / duration, 1); // improved easing (more dramatic stop) const easeOut = 1 - Math.pow(1 - progress, 5); // subtle vibration const wiggle = Math.sin(ts / 60) * (1 - progress) * 1.5; const y = finalPos * easeOut + wiggle; // 🎵 tick sound logic const currentTick = Math.floor(y / ITEM_HEIGHT); if (currentTick !== lastTick) { lastTick = currentTick; // optional sound // new Audio('/tick.mp3').play(); } list.style.transform = `translate3d(0,-${y}px,0)`; if (progress < 1) { requestAnimationFrame(step); } else { // bounce finish list.style.transition = 'transform 700ms cubic-bezier(0.34,1.56,0.64,1)'; list.style.transform = `translate3d(0,-${finalPos}px,0)`; setTimeout(() => { boxEl.classList.add('jackpot'); list.classList.add('blurred'); resolve(winner); }, 400); } }; requestAnimationFrame(step); }); }, resetSpinner(boxEl) { const list = boxEl.querySelector('.list'); // reset transform + animation state list.style.transition = 'none'; list.style.transform = 'translate3d(0,0,0)'; // force reflow (VERY IMPORTANT) void list.offsetHeight; list.classList.remove('blurred'); const items = list.querySelectorAll('.item'); items.forEach(i => i.classList.remove('winner-item')); boxEl.classList.remove('jackpot'); }, async animatePrize(boxEl, prizeIndex, winnerCount) { // reset prize winners this.winnersByPrize[prizeIndex] = []; const prizeWinners = []; const winnerText = boxEl.querySelector('.winner-text'); // reset UI winnerText.innerHTML = ''; winnerText.style.opacity = 0; winnerText.classList.remove('show'); const list = boxEl.querySelector('.list'); for (let j = 0; j < winnerCount; j++) { console.error('XXX',j); list.classList.remove('blurred'); winnerText.innerHTML = ''; winnerText.style.opacity = 0; winnerText.classList.remove('show'); const winner = await this.animateSingleWinner( boxEl, prizeIndex, j ); if (winner) { this.winnersByPrize[prizeIndex].push(winner); prizeWinners.push(winner); // clear previous winner before showing new one winnerText.innerHTML = ''; //const line = `${winner.email} (${winner.full_name})`; const line = `Winner ${j + 1}:
${winner.email} (${winner.full_name})`; // optional smooth transition winnerText.style.opacity = 0; winnerText.classList.remove('show'); setTimeout(() => { winnerText.innerHTML = `
${line}
`; winnerText.style.opacity = 1; winnerText.classList.add('show'); }, 150); // delay if multiple winners if (j < winnerCount - 1) { setTimeout(() => this.simpleConfetti(), 300); await new Promise(r => setTimeout(r, 2000)); winnerText.style.opacity = 0; winnerText.innerHTML = ''; this.resetSpinner(boxEl); } }else{ list.classList.add('blurred'); winnerText.innerHTML = ''; const line = `Winner ${j+1}:
Not selected`; // optional smooth transition winnerText.style.opacity = 0; winnerText.classList.remove('show'); setTimeout(() => { winnerText.innerHTML = `
${line}
`; winnerText.style.opacity = 1; winnerText.classList.add('show'); }, 150); await new Promise(r => setTimeout(r, 3000)); } {{-- // delay if multiple winners if (j < winnerCount - 1) { setTimeout(() => this.simpleConfetti(), 300); await new Promise(r => setTimeout(r, 2000)); winnerText.style.opacity = 0; winnerText.innerHTML = ''; this.resetSpinner(boxEl); } --}} } // confetti after all winners if (prizeWinners === winnerCount) { setTimeout(() => this.simpleConfetti(), 300); } return prizeWinners; }, async startFlow() { // 1. Intro this.screen = 'intro' await this.$nextTick(); await this.startScreenRecording(); await this.sleep(500) // 2. Prizes this.screen = 'prizes' this.startPrizeCarousel() await this.sleep(5000) this.stopPrizeCarousel() // 3. Draw (FIXED) this.activePrizeIndex = 0; this.screen = 'draw' await this.$nextTick() await this.startDraw() // 4. Results this.screen = 'results' await this.saveWinnersToDB(); await new Promise(resolve => setTimeout(resolve, 3000)); await this.stopScreenRecording(); }, startPrizeCarousel() { this.activePrizeSlide = 0; this.prizeInterval = setInterval(() => { this.activePrizeSlide++; if (this.activePrizeSlide >= this.prizes.length) { this.activePrizeSlide = 0; } }, 2500); // HOLD + MOVE timing }, stopPrizeCarousel() { clearInterval(this.prizeInterval); }, sleep(ms) { return new Promise(r => setTimeout(r, ms)) }, async startDraw() { this.activePrizeIndex = 0; this.drawStarted = true; {{-- await this.startScreenRecording(); await new Promise(resolve => setTimeout(resolve, 3000)); --}} this.winners = []; this.spinning = Array(this.prizes.length).fill(true); this.winnersByPrize = this.prizes.map(() => []); // smooth initial start await new Promise(r => setTimeout(r, 1500)); for (let i = 0; i < this.prizes.length; i++) { // activate current prize this.activePrizeIndex = i; await this.$nextTick(); // wait DOM update const boxEl = document.querySelector('.box'); // FULL RESET before each prize this.resetSpinner(boxEl); const winnerText = boxEl.querySelector('.winner-text'); winnerText.innerHTML = ''; winnerText.style.opacity = 0; winnerText.classList.remove('show'); const prize = this.prizes[i]; const winners = await this.animatePrize( boxEl, i, prize.winner_count || 1 ); this.winnersByPrize[i] = winners; // HOLD RESULT FOR 3 SECONDS await new Promise(r => setTimeout(r, 3000)); } // finished this.activePrizeIndex = -1; this.spinning = Array(this.prizes.length).fill(false); //await this.saveWinnersToDB(); {{-- await this.saveWinnersToDB(); await new Promise(resolve => setTimeout(resolve, 1000)); await this.stopScreenRecording(); --}} }, resetDraw() { this.completedPrizes = []; this.winners = []; this.winnersByPrize = this.prizes.map(() => []); // Reset winner groups this.spinning = Array(this.prizes.length).fill(false); this.generateNewWinners(); document.querySelectorAll('.box').forEach(box => { box.classList.remove('jackpot'); const list = box.querySelector('.list'); list.style.transform = 'translateY(0)'; list.classList.remove('blurred'); const winnerText = box.querySelector('.winner-text'); winnerText.style.opacity = 0; winnerText.classList.remove('show'); winnerText.innerHTML = ''; }); }, generateNewWinners() { let availableUsers = [...this.users]; // Shuffle users availableUsers.sort(() => Math.random() - 0.5); let newWinners = {}; this.prizes.forEach((prize) => { const count = prize.winner_count || 1; if (!newWinners[prize.id]) { newWinners[prize.id] = []; } for (let i = 0; i < count; i++) { if (availableUsers.length === 0) break; // Pick & REMOVE user (ensures no duplicates globally) const selected = availableUsers.shift(); newWinners[prize.id].push(selected); } }); this.serverWinners = newWinners; }, async startScreenRecording() { try { this.recordingCancelled = false; // 1. Get screen this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); const modal = document.getElementById('recordArea'); const rect = modal.getBoundingClientRect(); // 3. Create hidden video for screen frames videoEl = document.createElement('video'); videoEl.srcObject = this.screenStream; await videoEl.play(); // 4. Canvas setup const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = rect.width; canvas.height = rect.height; function draw() { if (!animationRunning) return; const videoWidth = videoEl.videoWidth; const videoHeight = videoEl.videoHeight; const scaleX = videoWidth / window.innerWidth; const scaleY = videoHeight / window.innerHeight; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage( videoEl, rect.left * scaleX, rect.top * scaleY, rect.width * scaleX, rect.height * scaleY, 0, 0, canvas.width, canvas.height ); rafId = requestAnimationFrame(draw); } animationRunning = true; draw(); canvasStream = canvas.captureStream(30); // 5. Combine audio tracks const tracks = [ ...canvasStream.getVideoTracks(), ...this.screenStream.getAudioTracks() ]; const finalStream = new MediaStream(tracks); // 6. Recorder this.mediaRecorder = new MediaRecorder(finalStream, { mimeType: 'video/webm;codecs=vp9' }); this.mediaRecorder.ondataavailable = e => { if (e.data.size > 0) this.recordedChunks.push(e.data); }; this.mediaRecorder.onstop = () => { const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); {{-- const preview = document.getElementById('preview'); preview.src = url; const a = document.createElement('a'); a.href = url; a.download = 'recording.webm'; a.click(); --}} // cleanup blob url after delay setTimeout(() => URL.revokeObjectURL(url), 5000); }; this.mediaRecorder.start(1000); this.isRecording = true; console.log('Recording started'); } catch (err) { this.recordingCancelled = true; console.error('Recording error:', err); this.$wire.showNotification( 'danger', 'Permission Denied', 'Screen recording permission was denied' ); } }, async stopScreenRecording() { return new Promise((resolve) => { this.isRecording = false; if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } if (!this.mediaRecorder) { resolve(); return; } this.mediaRecorder.onstop = async () => { if (this.recordingCancelled) { console.warn('Recording cancelled'); resolve(); return; } if (!this.recordedChunks.length) { console.warn('No recording data'); resolve(); return; } const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); const file = new File( [blob], `contest-draw-${Date.now()}.webm`, { type: 'video/webm' } ); // upload this.$wire.upload( 'video_url', file, () => console.log('Upload complete'), (err) => console.error('Upload error', err), (progress) => console.log(progress.detail.progress) ); resolve(); }; if (this.mediaRecorder.state !== 'inactive') { this.mediaRecorder.stop(); } if (this.screenStream) { this.screenStream.getTracks().forEach(track => track.stop()); } }); }, simpleConfetti() { const container = document.querySelector('.draw-winner'); const box = document.querySelector('.box'); if (!container || !box) return; const containerRect = container.getBoundingClientRect(); const boxRect = box.getBoundingClientRect(); const originX = boxRect.left - containerRect.left + boxRect.width / 2; const originY = boxRect.top - containerRect.top + boxRect.height / 2; const colors = ['#ef4444', '#22c55e', '#3b82f6', '#facc15', '#ffffff']; for (let i = 0; i < 100; i++) { const confetti = document.createElement('div'); container.appendChild(confetti); const size = Math.random() * 6 + 4; confetti.style.position = 'absolute'; confetti.style.width = size + 'px'; confetti.style.height = size + 'px'; confetti.style.background = colors[Math.floor(Math.random() * colors.length)]; confetti.style.left = originX + 'px'; confetti.style.top = originY + 'px'; confetti.style.borderRadius = '2px'; confetti.style.pointerEvents = 'none'; confetti.style.zIndex = 1000; const angle = Math.random() * 2 * Math.PI; const velocity = Math.random() * 8 + 4; let xVel = Math.cos(angle) * velocity; let yVel = Math.sin(angle) * velocity - 6; let x = 0; let y = 0; let gravity = 0.35; let rotation = Math.random() * 360; let rotateSpeed = (Math.random() - 0.5) * 20; const animate = () => { x += xVel; y += yVel; yVel += gravity; rotation += rotateSpeed; confetti.style.transform = ` translate(${x}px, ${y}px) rotate(${rotation}deg) `; confetti.style.opacity = 1 - (y / containerRect.height); if (y < containerRect.height) { requestAnimationFrame(animate); } else { confetti.remove(); } }; requestAnimationFrame(animate); } }, async saveResults() { console.error(this.winners); if (this.saving) return; this.saving = true; try { const payload = this.matchList.map(m => ({ id: m.id, selected: m.selected })); const response = await this.$wire.saveMatchResults(payload); if (response?.success) { this.serverWinners = response.winners; this.$dispatch('notify', { type: 'success', message: 'Results saved successfully!' }); this.screen = 'intro'; } } catch (error) { console.error(error); this.$dispatch('notify', { type: 'danger', message: 'Something went wrong while saving results' }); } finally { this.saving = false; } }, getPrizeWinners(prize) { const prizeId = prize.id; const winners = this.serverWinners?.[prizeId] ?? []; const expected = prize.winner_count || 1; // fill missing slots const filled = []; for (let i = 0; i < expected; i++) { filled.push(winners[i] ?? null); } return filled; } }" x-init="init()" class="p-3 space-y-3 record-screen">
{{-- --}} {{--
--}}
Recording...
Draw Type
{{ $draw->draw_type === 'random_draw' ? 'Random Draw' : 'Score Rank' }}
Save Result
Contest
{{ $contest->title }}
Draw Type
{{ $draw->draw_type === 'random_draw' ? 'Random Draw' : 'Score Rank' }}
Eligible
{{ $participants->count() ?? '0' }}
Start Draw {{-- Reset Draw --}}

🏆 Prizes

Draw

🎁 Winner List