Fix
This commit is contained in:
parent
fb6e2571a5
commit
50a8f4a295
@ -26,16 +26,24 @@ interface WinnerHistory {
|
|||||||
|
|
||||||
const RandomDrawApp: React.FC = () => {
|
const RandomDrawApp: React.FC = () => {
|
||||||
const [spinRows, setSpinRows] = useState<SpinRow[]>([]);
|
const [spinRows, setSpinRows] = useState<SpinRow[]>([]);
|
||||||
const [numberOfRows, setNumberOfRows] = useState<number>(4);
|
const [numberOfRows, setNumberOfRows] = useState<number>(5);
|
||||||
const [winnersHistory, setWinnersHistory] = useState<WinnerHistory[]>([]);
|
const [winnersHistory, setWinnersHistory] = useState<WinnerHistory[]>([]);
|
||||||
const [isRevealingWinners, setIsRevealingWinners] = useState<boolean>(false);
|
const [isRevealingWinners, setIsRevealingWinners] = useState<boolean>(false);
|
||||||
const [revealedWinners, setRevealedWinners] = useState<Set<number>>(new Set());
|
const [revealedWinners, setRevealedWinners] = useState<Set<number>>(new Set());
|
||||||
|
const [isSequentialSpinning, setIsSequentialSpinning] = useState<boolean>(false);
|
||||||
|
const [currentSpinningRow, setCurrentSpinningRow] = useState<number | null>(null);
|
||||||
|
const [selectedPrize, setSelectedPrize] = useState<number>(1); // 1 for Emas 0.25 Gram, 2 for Emas 3 Gram
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const tickIntervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
const tickIntervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
const shuffleIntervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
const shuffleIntervalRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
|
|
||||||
// Use React Query hook for fetching voucher data
|
// Use React Query hook for fetching voucher data - disabled by default
|
||||||
const { data: voucherData, isLoading: isLoadingData, refetch: refetchVouchers, error: fetchError } = useVoucherRows({ rows: numberOfRows });
|
const { data: voucherData, isLoading: isLoadingData, refetch: refetchVouchers, error: fetchError } = useVoucherRows({
|
||||||
|
rows: numberOfRows,
|
||||||
|
winner_number: selectedPrize
|
||||||
|
}, {
|
||||||
|
enabled: false // Disable automatic fetching - only fetch when Load Data is clicked
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize Audio Context
|
// Initialize Audio Context
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -44,9 +52,9 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('click', initAudio, { once: true });
|
document.addEventListener('click', initAudio, { once: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', initAudio);
|
document.removeEventListener('click', initAudio);
|
||||||
tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
||||||
@ -57,21 +65,21 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
// Create tick sound effect
|
// Create tick sound effect
|
||||||
const playTickSound = () => {
|
const playTickSound = () => {
|
||||||
if (!audioContextRef.current) return;
|
if (!audioContextRef.current) return;
|
||||||
|
|
||||||
const ctx = audioContextRef.current;
|
const ctx = audioContextRef.current;
|
||||||
const oscillator = ctx.createOscillator();
|
const oscillator = ctx.createOscillator();
|
||||||
const gainNode = ctx.createGain();
|
const gainNode = ctx.createGain();
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
oscillator.connect(gainNode);
|
||||||
gainNode.connect(ctx.destination);
|
gainNode.connect(ctx.destination);
|
||||||
|
|
||||||
oscillator.frequency.setValueAtTime(800, ctx.currentTime);
|
oscillator.frequency.setValueAtTime(800, ctx.currentTime);
|
||||||
oscillator.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.05);
|
oscillator.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.05);
|
||||||
|
|
||||||
gainNode.gain.setValueAtTime(0, ctx.currentTime);
|
gainNode.gain.setValueAtTime(0, ctx.currentTime);
|
||||||
gainNode.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.01);
|
gainNode.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.01);
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
|
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
|
||||||
|
|
||||||
oscillator.start(ctx.currentTime);
|
oscillator.start(ctx.currentTime);
|
||||||
oscillator.stop(ctx.currentTime + 0.05);
|
oscillator.stop(ctx.currentTime + 0.05);
|
||||||
};
|
};
|
||||||
@ -79,12 +87,12 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
// Create drum roll sound
|
// Create drum roll sound
|
||||||
const playDrumRoll = () => {
|
const playDrumRoll = () => {
|
||||||
if (!audioContextRef.current) return;
|
if (!audioContextRef.current) return;
|
||||||
|
|
||||||
const ctx = audioContextRef.current;
|
const ctx = audioContextRef.current;
|
||||||
const oscillator = ctx.createOscillator();
|
const oscillator = ctx.createOscillator();
|
||||||
const gainNode = ctx.createGain();
|
const gainNode = ctx.createGain();
|
||||||
const noiseNode = ctx.createBufferSource();
|
const noiseNode = ctx.createBufferSource();
|
||||||
|
|
||||||
// Create noise buffer for snare-like sound
|
// Create noise buffer for snare-like sound
|
||||||
const bufferSize = ctx.sampleRate * 0.05;
|
const bufferSize = ctx.sampleRate * 0.05;
|
||||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||||
@ -93,15 +101,15 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
data[i] = Math.random() * 2 - 1;
|
data[i] = Math.random() * 2 - 1;
|
||||||
}
|
}
|
||||||
noiseNode.buffer = buffer;
|
noiseNode.buffer = buffer;
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
oscillator.connect(gainNode);
|
||||||
noiseNode.connect(gainNode);
|
noiseNode.connect(gainNode);
|
||||||
gainNode.connect(ctx.destination);
|
gainNode.connect(ctx.destination);
|
||||||
|
|
||||||
oscillator.frequency.setValueAtTime(200, ctx.currentTime);
|
oscillator.frequency.setValueAtTime(200, ctx.currentTime);
|
||||||
gainNode.gain.setValueAtTime(0.05, ctx.currentTime);
|
gainNode.gain.setValueAtTime(0.05, ctx.currentTime);
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
|
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
|
||||||
|
|
||||||
oscillator.start(ctx.currentTime);
|
oscillator.start(ctx.currentTime);
|
||||||
oscillator.stop(ctx.currentTime + 0.1);
|
oscillator.stop(ctx.currentTime + 0.1);
|
||||||
noiseNode.start(ctx.currentTime);
|
noiseNode.start(ctx.currentTime);
|
||||||
@ -110,23 +118,23 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
// Create winner sound
|
// Create winner sound
|
||||||
const playWinnerSound = () => {
|
const playWinnerSound = () => {
|
||||||
if (!audioContextRef.current) return;
|
if (!audioContextRef.current) return;
|
||||||
|
|
||||||
const ctx = audioContextRef.current;
|
const ctx = audioContextRef.current;
|
||||||
const notes = [523.25, 659.25, 783.99, 1046.5]; // C, E, G, C (higher octave)
|
const notes = [523.25, 659.25, 783.99, 1046.5]; // C, E, G, C (higher octave)
|
||||||
|
|
||||||
notes.forEach((freq, index) => {
|
notes.forEach((freq, index) => {
|
||||||
const oscillator = ctx.createOscillator();
|
const oscillator = ctx.createOscillator();
|
||||||
const gainNode = ctx.createGain();
|
const gainNode = ctx.createGain();
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
oscillator.connect(gainNode);
|
||||||
gainNode.connect(ctx.destination);
|
gainNode.connect(ctx.destination);
|
||||||
|
|
||||||
oscillator.frequency.setValueAtTime(freq, ctx.currentTime + index * 0.1);
|
oscillator.frequency.setValueAtTime(freq, ctx.currentTime + index * 0.1);
|
||||||
|
|
||||||
gainNode.gain.setValueAtTime(0, ctx.currentTime + index * 0.1);
|
gainNode.gain.setValueAtTime(0, ctx.currentTime + index * 0.1);
|
||||||
gainNode.gain.linearRampToValueAtTime(0.2, ctx.currentTime + index * 0.1 + 0.05);
|
gainNode.gain.linearRampToValueAtTime(0.2, ctx.currentTime + index * 0.1 + 0.05);
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + index * 0.1 + 0.5);
|
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + index * 0.1 + 0.5);
|
||||||
|
|
||||||
oscillator.start(ctx.currentTime + index * 0.1);
|
oscillator.start(ctx.currentTime + index * 0.1);
|
||||||
oscillator.stop(ctx.currentTime + index * 0.1 + 0.5);
|
oscillator.stop(ctx.currentTime + index * 0.1 + 0.5);
|
||||||
});
|
});
|
||||||
@ -138,23 +146,23 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
if (existingInterval) {
|
if (existingInterval) {
|
||||||
clearTimeout(existingInterval);
|
clearTimeout(existingInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tickInterval = 50;
|
let tickInterval = 50;
|
||||||
const maxInterval = 200;
|
const maxInterval = 200;
|
||||||
const intervalIncrement = (maxInterval - tickInterval) / (duration / 100);
|
const intervalIncrement = (maxInterval - tickInterval) / (duration / 100);
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
playTickSound();
|
playTickSound();
|
||||||
tickInterval += intervalIncrement;
|
tickInterval += intervalIncrement;
|
||||||
|
|
||||||
if (tickInterval < maxInterval) {
|
if (tickInterval < maxInterval) {
|
||||||
const timeoutId = setTimeout(tick, Math.min(tickInterval, maxInterval));
|
const timeoutId = setTimeout(tick, Math.min(tickInterval, maxInterval));
|
||||||
tickIntervalRefs.current.set(rowId, timeoutId);
|
tickIntervalRefs.current.set(rowId, timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const interval = tickIntervalRefs.current.get(rowId);
|
const interval = tickIntervalRefs.current.get(rowId);
|
||||||
if (interval) {
|
if (interval) {
|
||||||
@ -200,10 +208,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchError]);
|
}, [fetchError]);
|
||||||
|
|
||||||
// Refetch when numberOfRows changes
|
// Remove auto-refetch - data will only load when Load Data button is clicked
|
||||||
useEffect(() => {
|
|
||||||
refetchVouchers();
|
|
||||||
}, [numberOfRows]);
|
|
||||||
|
|
||||||
// Shuffle all rows (Acak functionality)
|
// Shuffle all rows (Acak functionality)
|
||||||
const shuffleAllRows = () => {
|
const shuffleAllRows = () => {
|
||||||
@ -216,10 +221,10 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
spinRows.forEach((row, index) => {
|
spinRows.forEach((row, index) => {
|
||||||
let shuffleCount = 0;
|
let shuffleCount = 0;
|
||||||
const maxShuffles = 20;
|
const maxShuffles = 20;
|
||||||
|
|
||||||
const shuffleInterval = setInterval(() => {
|
const shuffleInterval = setInterval(() => {
|
||||||
shuffleCount++;
|
shuffleCount++;
|
||||||
|
|
||||||
setSpinRows(prev => prev.map(r => {
|
setSpinRows(prev => prev.map(r => {
|
||||||
if (r.id === row.id) {
|
if (r.id === row.id) {
|
||||||
return {
|
return {
|
||||||
@ -229,9 +234,9 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
playTickSound();
|
playTickSound();
|
||||||
|
|
||||||
if (shuffleCount >= maxShuffles) {
|
if (shuffleCount >= maxShuffles) {
|
||||||
clearInterval(shuffleInterval);
|
clearInterval(shuffleInterval);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -247,7 +252,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
}, index * 100);
|
}, index * 100);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
shuffleIntervalRefs.current.set(row.id, shuffleInterval as any);
|
shuffleIntervalRefs.current.set(row.id, shuffleInterval as any);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -256,47 +261,47 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
const revealWinnersDramatically = async () => {
|
const revealWinnersDramatically = async () => {
|
||||||
setIsRevealingWinners(true);
|
setIsRevealingWinners(true);
|
||||||
setRevealedWinners(new Set());
|
setRevealedWinners(new Set());
|
||||||
|
|
||||||
// First, arrange winners to be in the winner zone for each row
|
// First, arrange winners to be in the winner zone for each row
|
||||||
const arrangedRows = spinRows.map(row => {
|
const arrangedRows = spinRows.map(row => {
|
||||||
const winner = row.vouchers.find(v => v.is_winner);
|
const winner = row.vouchers.find(v => v.is_winner);
|
||||||
if (!winner) return row;
|
if (!winner) return row;
|
||||||
|
|
||||||
// Create a new display order with winner at position 3 or 4 (will be in winner zone)
|
// Create a new display order with winner at position 3 or 4 (will be in winner zone)
|
||||||
const otherVouchers = row.vouchers.filter(v => v.voucher_code !== winner.voucher_code);
|
const otherVouchers = row.vouchers.filter(v => v.voucher_code !== winner.voucher_code);
|
||||||
const shuffledOthers = shuffleArray(otherVouchers);
|
const shuffledOthers = shuffleArray(otherVouchers);
|
||||||
|
|
||||||
// Place winner at index 3 (4th position) which will be in the winner zone
|
// Place winner at index 3 (4th position) which will be in the winner zone
|
||||||
const newDisplayVouchers = [
|
const newDisplayVouchers = [
|
||||||
...shuffledOthers.slice(0, 3),
|
...shuffledOthers.slice(0, 3),
|
||||||
winner,
|
winner,
|
||||||
...shuffledOthers.slice(3)
|
...shuffledOthers.slice(3)
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
displayVouchers: newDisplayVouchers
|
displayVouchers: newDisplayVouchers
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update display to show arranged vouchers
|
// Update display to show arranged vouchers
|
||||||
setSpinRows(arrangedRows);
|
setSpinRows(arrangedRows);
|
||||||
|
|
||||||
// Start drum roll effect
|
// Start drum roll effect
|
||||||
const drumRollInterval = setInterval(() => {
|
const drumRollInterval = setInterval(() => {
|
||||||
playDrumRoll();
|
playDrumRoll();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// Create a scrolling/spinning effect for all wheels simultaneously
|
// Create a scrolling/spinning effect for all wheels simultaneously
|
||||||
let scrollSpeed = 50;
|
let scrollSpeed = 50;
|
||||||
let currentScroll = 0;
|
let currentScroll = 0;
|
||||||
const maxScroll = 2000;
|
const maxScroll = 2000;
|
||||||
const deceleration = 0.95;
|
const deceleration = 0.95;
|
||||||
|
|
||||||
const spinInterval = setInterval(() => {
|
const spinInterval = setInterval(() => {
|
||||||
currentScroll += scrollSpeed;
|
currentScroll += scrollSpeed;
|
||||||
scrollSpeed *= deceleration;
|
scrollSpeed *= deceleration;
|
||||||
|
|
||||||
// Apply scrolling to all wheels
|
// Apply scrolling to all wheels
|
||||||
arrangedRows.forEach(row => {
|
arrangedRows.forEach(row => {
|
||||||
const wheelElement = row.wheelRef.current;
|
const wheelElement = row.wheelRef.current;
|
||||||
@ -305,12 +310,12 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
wheelElement.style.transform = `translateY(${-currentScroll}px)`;
|
wheelElement.style.transform = `translateY(${-currentScroll}px)`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop when speed is very slow
|
// Stop when speed is very slow
|
||||||
if (scrollSpeed < 0.5 || currentScroll >= maxScroll) {
|
if (scrollSpeed < 0.5 || currentScroll >= maxScroll) {
|
||||||
clearInterval(spinInterval);
|
clearInterval(spinInterval);
|
||||||
clearInterval(drumRollInterval);
|
clearInterval(drumRollInterval);
|
||||||
|
|
||||||
// Final positioning - snap to winner position
|
// Final positioning - snap to winner position
|
||||||
arrangedRows.forEach(row => {
|
arrangedRows.forEach(row => {
|
||||||
const wheelElement = row.wheelRef.current;
|
const wheelElement = row.wheelRef.current;
|
||||||
@ -319,20 +324,20 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
const WINNER_ZONE_TOP = 260; // Top position of winner zone
|
const WINNER_ZONE_TOP = 260; // Top position of winner zone
|
||||||
// Position 4th card (index 3) exactly at the winner zone
|
// Position 4th card (index 3) exactly at the winner zone
|
||||||
const finalPosition = -(3 * CARD_HEIGHT - WINNER_ZONE_TOP);
|
const finalPosition = -(3 * CARD_HEIGHT - WINNER_ZONE_TOP);
|
||||||
|
|
||||||
wheelElement.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
wheelElement.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
||||||
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 20);
|
}, 20);
|
||||||
|
|
||||||
// Wait for scrolling to complete
|
// Wait for scrolling to complete
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
// Play winner sound
|
// Play winner sound
|
||||||
playWinnerSound();
|
playWinnerSound();
|
||||||
|
|
||||||
// Collect all winners
|
// Collect all winners
|
||||||
const winnersToReveal: { row: SpinRow, winner: Voucher }[] = [];
|
const winnersToReveal: { row: SpinRow, winner: Voucher }[] = [];
|
||||||
arrangedRows.forEach(row => {
|
arrangedRows.forEach(row => {
|
||||||
@ -341,11 +346,11 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
winnersToReveal.push({ row, winner });
|
winnersToReveal.push({ row, winner });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update all winners at once with highlighting
|
// Update all winners at once with highlighting
|
||||||
const allWinnerNumbers = new Set<number>();
|
const allWinnerNumbers = new Set<number>();
|
||||||
const historyEntries: WinnerHistory[] = [];
|
const historyEntries: WinnerHistory[] = [];
|
||||||
|
|
||||||
winnersToReveal.forEach(({ row, winner }) => {
|
winnersToReveal.forEach(({ row, winner }) => {
|
||||||
allWinnerNumbers.add(row.rowNumber);
|
allWinnerNumbers.add(row.rowNumber);
|
||||||
historyEntries.push({
|
historyEntries.push({
|
||||||
@ -354,10 +359,10 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update state to highlight winners
|
// Update state to highlight winners
|
||||||
setRevealedWinners(allWinnerNumbers);
|
setRevealedWinners(allWinnerNumbers);
|
||||||
|
|
||||||
setSpinRows(prev => prev.map(r => {
|
setSpinRows(prev => prev.map(r => {
|
||||||
const winnerInfo = winnersToReveal.find(w => w.row.id === r.id);
|
const winnerInfo = winnersToReveal.find(w => w.row.id === r.id);
|
||||||
if (winnerInfo) {
|
if (winnerInfo) {
|
||||||
@ -369,12 +374,112 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setWinnersHistory(prev => [...prev, ...historyEntries]);
|
setWinnersHistory(prev => [...prev, ...historyEntries]);
|
||||||
|
|
||||||
setIsRevealingWinners(false);
|
setIsRevealingWinners(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const performDramaticSpin = (row: SpinRow, isSequential: boolean = false) => {
|
||||||
|
const wheelElement = row.wheelRef.current;
|
||||||
|
if (!wheelElement) return;
|
||||||
|
|
||||||
|
const winner = row.vouchers.find(v => v.is_winner);
|
||||||
|
if (!winner) {
|
||||||
|
console.error('No winner found in vouchers');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARD_HEIGHT = 80;
|
||||||
|
const WINNER_ZONE_TOP = 260; // Top position of winner zone
|
||||||
|
|
||||||
|
// Shuffle the display vouchers to create more dramatic effect
|
||||||
|
const shuffledVouchers = shuffleArray([...row.vouchers]);
|
||||||
|
const winnerIndex = shuffledVouchers.findIndex(v => v.voucher_code === winner.voucher_code);
|
||||||
|
|
||||||
|
// Update display vouchers with shuffled order
|
||||||
|
setSpinRows(prev => prev.map(r =>
|
||||||
|
r.id === row.id ? { ...r, displayVouchers: shuffledVouchers } : r
|
||||||
|
));
|
||||||
|
|
||||||
|
wheelElement.style.transition = 'none';
|
||||||
|
wheelElement.style.transform = 'translateY(0px)';
|
||||||
|
wheelElement.offsetHeight;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// More dramatic spinning with multiple phases
|
||||||
|
const minSpins = 15;
|
||||||
|
const maxSpins = 25;
|
||||||
|
const spins = minSpins + Math.random() * (maxSpins - minSpins);
|
||||||
|
const totalItems = shuffledVouchers.length;
|
||||||
|
|
||||||
|
const totalRotations = Math.floor(spins);
|
||||||
|
const baseScrollDistance = totalRotations * totalItems * CARD_HEIGHT;
|
||||||
|
const winnerScrollPosition = winnerIndex * CARD_HEIGHT;
|
||||||
|
const finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_TOP;
|
||||||
|
|
||||||
|
const distance = Math.abs(finalPosition);
|
||||||
|
const baseDuration = 4000; // Longer base duration
|
||||||
|
const maxDuration = 7000; // Longer max duration
|
||||||
|
const duration = Math.min(baseDuration + (distance / 8000) * 2000, maxDuration);
|
||||||
|
|
||||||
|
// Phase 1: Fast initial spin
|
||||||
|
const phase1Duration = duration * 0.3;
|
||||||
|
const phase1Distance = finalPosition * 0.7;
|
||||||
|
|
||||||
|
wheelElement.style.transform = `translateY(${phase1Distance}px)`;
|
||||||
|
wheelElement.style.transition = `transform ${phase1Duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
|
||||||
|
|
||||||
|
// Phase 2: Slower approach to final position
|
||||||
|
setTimeout(() => {
|
||||||
|
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
||||||
|
wheelElement.style.transition = `transform ${duration - phase1Duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)`;
|
||||||
|
}, phase1Duration);
|
||||||
|
|
||||||
|
startTickSound(row.id, duration);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const interval = tickIntervalRefs.current.get(row.id);
|
||||||
|
if (interval) {
|
||||||
|
clearTimeout(interval);
|
||||||
|
tickIntervalRefs.current.delete(row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
playWinnerSound();
|
||||||
|
|
||||||
|
setSpinRows(prev => prev.map(r =>
|
||||||
|
r.id === row.id
|
||||||
|
? { ...r, isSpinning: false, winner: winner, selectedWinner: winner }
|
||||||
|
: r
|
||||||
|
));
|
||||||
|
|
||||||
|
setWinnersHistory(prev => [...prev, {
|
||||||
|
rowNumber: row.rowNumber,
|
||||||
|
winner: winner,
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// If this is sequential spinning, trigger next row
|
||||||
|
if (isSequential) {
|
||||||
|
const nextRowIndex = spinRows.findIndex(r => r.id === row.id) + 1;
|
||||||
|
if (nextRowIndex < spinRows.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
spinSequentialRow(nextRowIndex);
|
||||||
|
}, 2000); // Wait 2 seconds before next spin
|
||||||
|
} else {
|
||||||
|
// All rows completed
|
||||||
|
setIsSequentialSpinning(false);
|
||||||
|
setCurrentSpinningRow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the winner in the winner zone - don't reset position
|
||||||
|
// The winning card will stay exactly where it landed in the winner zone
|
||||||
|
|
||||||
|
}, duration);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
const performSpin = (row: SpinRow) => {
|
const performSpin = (row: SpinRow) => {
|
||||||
const wheelElement = row.wheelRef.current;
|
const wheelElement = row.wheelRef.current;
|
||||||
if (!wheelElement) return;
|
if (!wheelElement) return;
|
||||||
@ -387,89 +492,113 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
|
|
||||||
const CARD_HEIGHT = 80;
|
const CARD_HEIGHT = 80;
|
||||||
const WINNER_ZONE_TOP = 260; // Top position of winner zone
|
const WINNER_ZONE_TOP = 260; // Top position of winner zone
|
||||||
|
|
||||||
const winnerIndex = row.displayVouchers.findIndex(v => v.voucher_code === winner.voucher_code);
|
// Shuffle the display vouchers to create more dramatic effect
|
||||||
|
const shuffledVouchers = shuffleArray([...row.vouchers]);
|
||||||
|
const winnerIndex = shuffledVouchers.findIndex(v => v.voucher_code === winner.voucher_code);
|
||||||
|
|
||||||
|
// Update display vouchers with shuffled order
|
||||||
|
setSpinRows(prev => prev.map(r =>
|
||||||
|
r.id === row.id ? { ...r, displayVouchers: shuffledVouchers } : r
|
||||||
|
));
|
||||||
|
|
||||||
wheelElement.style.transition = 'none';
|
wheelElement.style.transition = 'none';
|
||||||
wheelElement.style.transform = 'translateY(0px)';
|
wheelElement.style.transform = 'translateY(0px)';
|
||||||
wheelElement.offsetHeight;
|
wheelElement.offsetHeight;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const minSpins = 10;
|
// More dramatic spinning with multiple phases
|
||||||
const maxSpins = 20;
|
const minSpins = 15;
|
||||||
|
const maxSpins = 25;
|
||||||
const spins = minSpins + Math.random() * (maxSpins - minSpins);
|
const spins = minSpins + Math.random() * (maxSpins - minSpins);
|
||||||
const totalItems = row.displayVouchers.length;
|
const totalItems = shuffledVouchers.length;
|
||||||
|
|
||||||
const totalRotations = Math.floor(spins);
|
const totalRotations = Math.floor(spins);
|
||||||
const baseScrollDistance = totalRotations * totalItems * CARD_HEIGHT;
|
const baseScrollDistance = totalRotations * totalItems * CARD_HEIGHT;
|
||||||
const winnerScrollPosition = winnerIndex * CARD_HEIGHT;
|
const winnerScrollPosition = winnerIndex * CARD_HEIGHT;
|
||||||
const finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_TOP;
|
const finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_TOP;
|
||||||
|
|
||||||
const distance = Math.abs(finalPosition);
|
const distance = Math.abs(finalPosition);
|
||||||
const baseDuration = 3000;
|
const baseDuration = 4000; // Longer base duration
|
||||||
const maxDuration = 5000;
|
const maxDuration = 7000; // Longer max duration
|
||||||
const duration = Math.min(baseDuration + (distance / 10000) * 1000, maxDuration);
|
const duration = Math.min(baseDuration + (distance / 8000) * 2000, maxDuration);
|
||||||
|
|
||||||
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
// Phase 1: Fast initial spin
|
||||||
wheelElement.style.transition = `transform ${duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)`;
|
const phase1Duration = duration * 0.3;
|
||||||
|
const phase1Distance = finalPosition * 0.7;
|
||||||
|
|
||||||
|
wheelElement.style.transform = `translateY(${phase1Distance}px)`;
|
||||||
|
wheelElement.style.transition = `transform ${phase1Duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
|
||||||
|
|
||||||
|
// Phase 2: Slower approach to final position
|
||||||
|
setTimeout(() => {
|
||||||
|
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
||||||
|
wheelElement.style.transition = `transform ${duration - phase1Duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)`;
|
||||||
|
}, phase1Duration);
|
||||||
|
|
||||||
startTickSound(row.id, duration);
|
startTickSound(row.id, duration);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const interval = tickIntervalRefs.current.get(row.id);
|
const interval = tickIntervalRefs.current.get(row.id);
|
||||||
if (interval) {
|
if (interval) {
|
||||||
clearTimeout(interval);
|
clearTimeout(interval);
|
||||||
tickIntervalRefs.current.delete(row.id);
|
tickIntervalRefs.current.delete(row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
playWinnerSound();
|
playWinnerSound();
|
||||||
|
|
||||||
setSpinRows(prev => prev.map(r =>
|
setSpinRows(prev => prev.map(r =>
|
||||||
r.id === row.id
|
r.id === row.id
|
||||||
? { ...r, isSpinning: false, winner: winner, selectedWinner: winner }
|
? { ...r, isSpinning: false, winner: winner, selectedWinner: winner }
|
||||||
: r
|
: r
|
||||||
));
|
));
|
||||||
|
|
||||||
setWinnersHistory(prev => [...prev, {
|
setWinnersHistory(prev => [...prev, {
|
||||||
rowNumber: row.rowNumber,
|
rowNumber: row.rowNumber,
|
||||||
winner: winner,
|
winner: winner,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
setTimeout(() => {
|
// Keep the winner in the winner zone - don't reset position
|
||||||
wheelElement.style.transition = 'none';
|
// The winning card will stay exactly where it landed in the winner zone
|
||||||
wheelElement.style.transform = 'translateY(0px)';
|
|
||||||
wheelElement.offsetHeight;
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
}, duration);
|
}, duration);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const spinWheel = (rowId: string) => {
|
const spinSequentialRow = (rowIndex: number) => {
|
||||||
const row = spinRows.find(r => r.id === rowId);
|
if (rowIndex >= spinRows.length) {
|
||||||
if (!row || row.isSpinning || row.winner) return;
|
setIsSequentialSpinning(false);
|
||||||
|
setCurrentSpinningRow(null);
|
||||||
setSpinRows(prev => prev.map(r =>
|
return;
|
||||||
r.id === rowId ? { ...r, isSpinning: true, selectedWinner: null } : r
|
}
|
||||||
|
|
||||||
|
const row = spinRows[rowIndex];
|
||||||
|
if (!row || row.isSpinning || row.winner) {
|
||||||
|
// Skip this row and go to next
|
||||||
|
spinSequentialRow(rowIndex + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSpinningRow(row.rowNumber);
|
||||||
|
setSpinRows(prev => prev.map(r =>
|
||||||
|
r.id === row.id ? { ...r, isSpinning: true, selectedWinner: null } : r
|
||||||
));
|
));
|
||||||
|
|
||||||
const wheelElement = row.wheelRef.current;
|
const wheelElement = row.wheelRef.current;
|
||||||
if (wheelElement) {
|
if (wheelElement) {
|
||||||
wheelElement.style.transition = 'none';
|
wheelElement.style.transition = 'none';
|
||||||
wheelElement.style.transform = 'translateY(0px)';
|
wheelElement.style.transform = 'translateY(0px)';
|
||||||
wheelElement.offsetHeight;
|
wheelElement.offsetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
performSpin(row);
|
performDramaticSpin(row, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearResults = () => {
|
const startSequentialSpinning = () => {
|
||||||
tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
if (spinRows.length === 0) return;
|
||||||
shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
|
||||||
tickIntervalRefs.current.clear();
|
// Clear any existing results
|
||||||
shuffleIntervalRefs.current.clear();
|
|
||||||
|
|
||||||
setWinnersHistory([]);
|
setWinnersHistory([]);
|
||||||
setRevealedWinners(new Set());
|
setRevealedWinners(new Set());
|
||||||
setSpinRows(prev => prev.map(row => ({
|
setSpinRows(prev => prev.map(row => ({
|
||||||
@ -480,7 +609,62 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
selectedWinner: null,
|
selectedWinner: null,
|
||||||
displayVouchers: [...row.vouchers]
|
displayVouchers: [...row.vouchers]
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
// Reset all wheel positions
|
||||||
|
spinRows.forEach(row => {
|
||||||
|
const wheelElement = row.wheelRef.current;
|
||||||
|
if (wheelElement) {
|
||||||
|
wheelElement.style.transition = 'none';
|
||||||
|
wheelElement.style.transform = 'translateY(0px)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSequentialSpinning(true);
|
||||||
|
setCurrentSpinningRow(null);
|
||||||
|
|
||||||
|
// Start with first row after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
spinSequentialRow(0);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const spinWheel = (rowId: string) => {
|
||||||
|
const row = spinRows.find(r => r.id === rowId);
|
||||||
|
if (!row || row.isSpinning || row.winner) return;
|
||||||
|
|
||||||
|
setSpinRows(prev => prev.map(r =>
|
||||||
|
r.id === rowId ? { ...r, isSpinning: true, selectedWinner: null } : r
|
||||||
|
));
|
||||||
|
|
||||||
|
const wheelElement = row.wheelRef.current;
|
||||||
|
if (wheelElement) {
|
||||||
|
wheelElement.style.transition = 'none';
|
||||||
|
wheelElement.style.transform = 'translateY(0px)';
|
||||||
|
wheelElement.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
performSpin(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearResults = () => {
|
||||||
|
tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
||||||
|
shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
||||||
|
tickIntervalRefs.current.clear();
|
||||||
|
shuffleIntervalRefs.current.clear();
|
||||||
|
|
||||||
|
setWinnersHistory([]);
|
||||||
|
setRevealedWinners(new Set());
|
||||||
|
setIsSequentialSpinning(false);
|
||||||
|
setCurrentSpinningRow(null);
|
||||||
|
setSpinRows(prev => prev.map(row => ({
|
||||||
|
...row,
|
||||||
|
isSpinning: false,
|
||||||
|
isShuffling: false,
|
||||||
|
winner: null,
|
||||||
|
selectedWinner: null,
|
||||||
|
displayVouchers: [...row.vouchers]
|
||||||
|
})));
|
||||||
|
|
||||||
spinRows.forEach(row => {
|
spinRows.forEach(row => {
|
||||||
const wheelElement = row.wheelRef.current;
|
const wheelElement = row.wheelRef.current;
|
||||||
if (wheelElement) {
|
if (wheelElement) {
|
||||||
@ -495,7 +679,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout));
|
||||||
tickIntervalRefs.current.clear();
|
tickIntervalRefs.current.clear();
|
||||||
shuffleIntervalRefs.current.clear();
|
shuffleIntervalRefs.current.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refetchVouchers();
|
await refetchVouchers();
|
||||||
toast.success('Data refreshed successfully');
|
toast.success('Data refreshed successfully');
|
||||||
@ -527,97 +711,110 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-bounce-in {
|
.animate-bounce-in {
|
||||||
animation: bounce-in 0.6s ease-out;
|
animation: bounce-in 0.6s ease-out;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div className="max-w-full px-4 mx-auto">
|
<div className="max-w-full px-4 mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-5xl font-bold bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent mb-2">
|
<h1 className="text-5xl font-bold bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent mb-2">
|
||||||
Voucher Lucky Draw
|
Voucher Lucky Draw
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 text-lg">Select random winners from voucher database</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Controls Section */}
|
{/* Controls Section - Compact Layout */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
||||||
{/* Configuration Card */}
|
{/* Configuration Card */}
|
||||||
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
<div className="bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900">
|
<h2 className="text-lg font-semibold mb-3 text-gray-900">
|
||||||
Configuration
|
Configuration
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<div>
|
||||||
Number of Rows
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
</label>
|
Number of Rows
|
||||||
<input
|
</label>
|
||||||
type="number"
|
<input
|
||||||
min="1"
|
type="number"
|
||||||
max="10"
|
min="1"
|
||||||
value={numberOfRows}
|
max="10"
|
||||||
onChange={(e) => setNumberOfRows(parseInt(e.target.value) || 1)}
|
value={numberOfRows}
|
||||||
className="w-full bg-white border border-gray-300 rounded-lg p-2 text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
onChange={(e) => setNumberOfRows(parseInt(e.target.value) || 1)}
|
||||||
disabled={isLoadingData}
|
className="w-full bg-white border border-gray-300 rounded p-1.5 text-sm text-gray-900 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||||
/>
|
disabled={isLoadingData}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
<button
|
|
||||||
onClick={refreshData}
|
<div>
|
||||||
disabled={isLoadingData}
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
className="w-full px-4 py-2 bg-primary hover:bg-primary/90 disabled:bg-gray-400 text-white font-semibold rounded-lg flex items-center justify-center gap-2 transition-colors"
|
Prize Selection
|
||||||
>
|
</label>
|
||||||
<RefreshCw size={16} className={isLoadingData ? 'animate-spin' : ''} />
|
<select
|
||||||
{isLoadingData ? 'Loading...' : 'Load Data'}
|
value={selectedPrize}
|
||||||
</button>
|
onChange={(e) => setSelectedPrize(parseInt(e.target.value))}
|
||||||
</div>
|
className="w-full bg-white border border-gray-300 rounded p-1.5 text-sm text-gray-900 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||||
|
disabled={isLoadingData}
|
||||||
{spinRows.length > 0 && (
|
>
|
||||||
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
<option value={1}>🥇 Emas 0.25 Gram</option>
|
||||||
<div className="text-sm text-gray-600">
|
<option value={2}>🥇 Emas 3 Gram</option>
|
||||||
<div>Rows: {spinRows.length}</div>
|
</select>
|
||||||
<div>Total Vouchers: {spinRows.reduce((acc, row) => acc + row.vouchers.length, 0)}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={refreshData}
|
||||||
|
disabled={isLoadingData}
|
||||||
|
className="w-full px-3 py-1.5 bg-primary hover:bg-primary/90 disabled:bg-gray-400 text-white text-sm font-semibold rounded flex items-center justify-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} className={isLoadingData ? 'animate-spin' : ''} />
|
||||||
|
{isLoadingData ? 'Loading...' : 'Load Data'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{spinRows.length > 0 && (
|
||||||
|
<div className="mt-3 p-2 bg-gray-50 rounded text-xs text-gray-600">
|
||||||
|
<div>Rows: {spinRows.length} | Vouchers: {spinRows.reduce((acc, row) => acc + row.vouchers.length, 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions Card */}
|
{/* Actions Card */}
|
||||||
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
<div className="bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900">
|
<h2 className="text-lg font-semibold mb-3 text-gray-900">
|
||||||
Actions
|
Actions
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<button
|
<button
|
||||||
onClick={shuffleAllRows}
|
onClick={shuffleAllRows}
|
||||||
disabled={isLoadingData || spinRows.some(r => r.isShuffling) || spinRows.length === 0}
|
disabled={isLoadingData || spinRows.some(r => r.isShuffling) || spinRows.length === 0}
|
||||||
className="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-semibold rounded-lg flex items-center justify-center gap-2 transition-colors"
|
className="w-full px-3 py-1.5 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white text-sm font-semibold rounded flex items-center justify-center gap-1 transition-colors"
|
||||||
>
|
>
|
||||||
<Shuffle size={16} className={spinRows.some(r => r.isShuffling) ? 'animate-pulse' : ''} />
|
<Shuffle size={14} className={spinRows.some(r => r.isShuffling) ? 'animate-pulse' : ''} />
|
||||||
Shuffle All Rows
|
Shuffle All
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={revealWinnersDramatically}
|
onClick={startSequentialSpinning}
|
||||||
disabled={isRevealingWinners || spinRows.length === 0}
|
disabled={isSequentialSpinning || spinRows.length === 0}
|
||||||
className="w-full px-4 py-2 bg-gradient-to-r from-yellow-400 to-orange-500 hover:from-yellow-500 hover:to-orange-600 disabled:from-gray-400 disabled:to-gray-500 text-white font-bold rounded-lg flex items-center justify-center gap-2 transition-all transform hover:scale-105"
|
className="w-full px-3 py-1.5 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 text-white text-sm font-bold rounded flex items-center justify-center gap-1 transition-all"
|
||||||
>
|
>
|
||||||
<Trophy size={16} className={isRevealingWinners ? 'animate-bounce' : ''} />
|
<Trophy size={14} className={isSequentialSpinning ? 'animate-spin' : ''} />
|
||||||
{isRevealingWinners ? 'Revealing...' : 'Reveal All Winners'}
|
{isSequentialSpinning ? 'Spinning...' : 'Spin for Winner'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{winnersHistory.length > 0 && (
|
{winnersHistory.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={clearResults}
|
onClick={clearResults}
|
||||||
className="w-full px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg flex items-center justify-center gap-2 transition-colors"
|
className="w-full px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded flex items-center justify-center gap-1 transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={14} />
|
||||||
Clear Results
|
Clear Results
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -625,18 +822,18 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Winners Summary Card */}
|
{/* Winners Summary Card */}
|
||||||
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
<div className="bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 flex items-center gap-2">
|
<h2 className="text-lg font-semibold mb-3 text-gray-900 flex items-center gap-2">
|
||||||
<Trophy className="text-yellow-500" size={24} />
|
<Trophy className="text-yellow-500" size={18} />
|
||||||
Winners
|
Winners
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{winnersHistory.length > 0 ? (
|
{winnersHistory.length > 0 ? (
|
||||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||||
{winnersHistory.map((history) => (
|
{winnersHistory.map((history) => (
|
||||||
<div
|
<div
|
||||||
key={`${history.rowNumber}-${history.timestamp.getTime()}`}
|
key={`${history.rowNumber}-${history.timestamp.getTime()}`}
|
||||||
className="p-2 bg-yellow-50 border border-yellow-200 rounded text-sm"
|
className="p-1.5 bg-yellow-50 border border-yellow-200 rounded text-xs"
|
||||||
>
|
>
|
||||||
<div className="font-semibold text-yellow-700">
|
<div className="font-semibold text-yellow-700">
|
||||||
Row {history.rowNumber}: {history.winner.name}
|
Row {history.rowNumber}: {history.winner.name}
|
||||||
@ -648,9 +845,9 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-2">
|
||||||
<Trophy className="mx-auto text-gray-300 mb-2" size={32} />
|
<Trophy className="mx-auto text-gray-300 mb-1" size={20} />
|
||||||
<p className="text-gray-500 text-sm">No winners yet</p>
|
<p className="text-gray-500 text-xs">No winners yet</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -670,22 +867,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
row.isShuffling ? 'border-purple-400 animate-pulse' : 'border-gray-200'
|
row.isShuffling ? 'border-purple-400 animate-pulse' : 'border-gray-200'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
|
||||||
Row {row.rowNumber}
|
|
||||||
<span className="text-sm text-gray-600 ml-1">({row.vouchers.length})</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => spinWheel(row.id)}
|
|
||||||
disabled={row.isSpinning || row.winner !== null || row.isShuffling}
|
|
||||||
className="w-full px-3 py-2 bg-primary hover:bg-primary/90 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold rounded-lg flex items-center justify-center gap-2 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<RotateCw size={14} className={row.isSpinning ? 'animate-spin' : ''} />
|
|
||||||
{row.isSpinning ? 'Spinning...' : row.winner ? 'Winner' : 'Spin'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{row.winner && (
|
{row.winner && (
|
||||||
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded-lg animate-bounce-in">
|
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded-lg animate-bounce-in">
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
@ -700,54 +882,57 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wheel Container */}
|
{/* Wheel Container */}
|
||||||
<div
|
<div
|
||||||
className="relative bg-gray-50 overflow-hidden"
|
className="relative bg-gray-50 overflow-hidden"
|
||||||
style={{ height: '600px' }}
|
style={{ height: '600px' }}
|
||||||
>
|
>
|
||||||
{/* Winner Selection Zone - Exactly 80px matching card height */}
|
{/* Subtle Winner Zone Indicator */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 z-50"
|
className="absolute left-0 right-0 z-10 pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
top: '260px',
|
top: '260px',
|
||||||
height: '80px',
|
height: '80px',
|
||||||
border: '3px solid #ef4444',
|
background: 'linear-gradient(90deg, transparent 0%, rgba(34, 197, 94, 0.1) 20%, rgba(34, 197, 94, 0.2) 50%, rgba(34, 197, 94, 0.1) 80%, transparent 100%)',
|
||||||
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
borderTop: '2px dashed rgba(34, 197, 94, 0.3)',
|
||||||
display: 'flex',
|
borderBottom: '2px dashed rgba(34, 197, 94, 0.3)'
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
paddingRight: '10px'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-red-600 text-white px-3 py-1 rounded font-bold text-xs tracking-wider shadow-lg">
|
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
|
||||||
WINNER
|
<div className="bg-green-500 text-white px-2 py-1 rounded text-xs font-bold shadow-lg">
|
||||||
|
🏆 WINNER
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrolling Cards */}
|
{/* Scrolling Cards */}
|
||||||
<div
|
<div
|
||||||
ref={row.wheelRef}
|
ref={row.wheelRef}
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{ transform: 'translateY(0px)' }}
|
style={{ transform: 'translateY(0px)' }}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 50 }, (_, repeatIndex) =>
|
{Array.from({ length: 50 }, (_, repeatIndex) =>
|
||||||
row.displayVouchers.map((voucher, voucherIndex) => {
|
row.displayVouchers.map((voucher, voucherIndex) => {
|
||||||
const isSelectedWinner = row.selectedWinner?.voucher_code === voucher.voucher_code;
|
const isSelectedWinner = row.selectedWinner?.voucher_code === voucher.voucher_code;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${repeatIndex}-${voucherIndex}`}
|
key={`${repeatIndex}-${voucherIndex}`}
|
||||||
className={`flex items-center px-4 border-b border-gray-300 transition-colors ${
|
className={`flex items-center px-4 border-b border-gray-300 transition-all duration-300 ${
|
||||||
isSelectedWinner ? 'bg-yellow-100' :
|
isSelectedWinner
|
||||||
voucherIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'
|
? 'bg-gradient-to-r from-yellow-100 to-yellow-200 border-yellow-300 shadow-lg'
|
||||||
}`}
|
: voucherIndex % 2 === 0
|
||||||
style={{
|
? 'bg-white hover:bg-gray-50'
|
||||||
height: '80px',
|
: 'bg-gray-50 hover:bg-gray-100'
|
||||||
minHeight: '80px',
|
}`}
|
||||||
maxHeight: '80px'
|
style={{
|
||||||
}}
|
height: '80px',
|
||||||
>
|
minHeight: '80px',
|
||||||
|
maxHeight: '80px',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className={`font-bold text-lg mb-1 ${
|
<div className={`font-bold text-lg mb-1 ${
|
||||||
isSelectedWinner ? 'text-yellow-600' : 'text-gray-900'
|
isSelectedWinner ? 'text-yellow-600' : 'text-gray-900'
|
||||||
@ -760,7 +945,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
{voucher.name}
|
{voucher.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-gray-500 text-xs">
|
<div className="text-gray-500 text-xs">
|
||||||
{formatPhoneNumber(voucher.phone_number)}
|
{formatPhoneNumber(voucher.phone_number)}
|
||||||
@ -774,7 +959,7 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gradient Overlays */}
|
{/* Gradient Overlays */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-gray-50 via-gray-50/70 to-transparent pointer-events-none z-40"></div>
|
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-gray-50 via-gray-50/70 to-transparent pointer-events-none z-40"></div>
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-gray-50 via-gray-50/70 to-transparent pointer-events-none z-40"></div>
|
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-gray-50 via-gray-50/70 to-transparent pointer-events-none z-40"></div>
|
||||||
@ -788,4 +973,4 @@ const RandomDrawApp: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RandomDrawApp;
|
export default RandomDrawApp;
|
||||||
|
|||||||
@ -4,28 +4,31 @@ import { api } from '../api'
|
|||||||
|
|
||||||
export interface VouchersQueryParams {
|
export interface VouchersQueryParams {
|
||||||
rows?: number
|
rows?: number
|
||||||
|
winner_number?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVoucherRows(params: VouchersQueryParams = {}) {
|
export function useVoucherRows(params: VouchersQueryParams = {}, options: { enabled?: boolean } = {}) {
|
||||||
const { rows = 4 } = params
|
const { rows = 5, winner_number = 1 } = params
|
||||||
|
const { enabled = true } = options
|
||||||
|
|
||||||
return useQuery<VoucherRowsResponse>({
|
return useQuery<VoucherRowsResponse>({
|
||||||
queryKey: ['voucher-rows', { rows }],
|
queryKey: ['voucher-rows', { rows, winner_number }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get(`/vouchers/rows`, {
|
const res = await api.get(`/vouchers/rows`, {
|
||||||
params: { rows }
|
params: { rows, winner_number }
|
||||||
})
|
})
|
||||||
return res.data.data
|
return res.data.data
|
||||||
},
|
},
|
||||||
|
enabled,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual fetch function for cases where you need to fetch without using the hook
|
// Manual fetch function for cases where you need to fetch without using the hook
|
||||||
export async function fetchVoucherRows(rows: number = 4): Promise<VoucherRowsResponse> {
|
export async function fetchVoucherRows(rows: number = 5, winner_number: number = 1): Promise<VoucherRowsResponse> {
|
||||||
const res = await api.get(`/vouchers/rows`, {
|
const res = await api.get(`/vouchers/rows`, {
|
||||||
params: { rows }
|
params: { rows, winner_number }
|
||||||
})
|
})
|
||||||
return res.data.data
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user