627 lines
24 KiB
TypeScript
627 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Users, RotateCw, Trophy, Trash2, RefreshCw, Database } from 'lucide-react';
|
|
|
|
interface Order {
|
|
orderId: string;
|
|
customerName: string;
|
|
email: string;
|
|
amount: number;
|
|
status: string;
|
|
}
|
|
|
|
interface WinnerResult {
|
|
order: Order;
|
|
timestamp: Date;
|
|
position: number;
|
|
}
|
|
|
|
const RandomDrawApp: React.FC = () => {
|
|
const [orders, setOrders] = useState<Order[]>([]);
|
|
const [isSpinning, setIsSpinning] = useState<boolean>(false);
|
|
const [winners, setWinners] = useState<WinnerResult[]>([]);
|
|
const [numberOfWinners, setNumberOfWinners] = useState<number>(1);
|
|
const [currentWheelItems, setCurrentWheelItems] = useState<Order[]>([]);
|
|
const [selectedWinner, setSelectedWinner] = useState<Order | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'acak' | 'hadiah' | 'winner'>('acak');
|
|
const [shuffledDisplayOrder, setShuffledDisplayOrder] = useState<Order[]>([]);
|
|
const wheelRef = useRef<HTMLDivElement>(null);
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
const tickIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Initialize Audio Context
|
|
useEffect(() => {
|
|
const initAudio = () => {
|
|
if (!audioContextRef.current) {
|
|
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', initAudio, { once: true });
|
|
|
|
return () => {
|
|
document.removeEventListener('click', initAudio);
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Create tick sound effect
|
|
const playTickSound = () => {
|
|
if (!audioContextRef.current) return;
|
|
|
|
const ctx = audioContextRef.current;
|
|
const oscillator = ctx.createOscillator();
|
|
const gainNode = ctx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(ctx.destination);
|
|
|
|
oscillator.frequency.setValueAtTime(800, ctx.currentTime);
|
|
oscillator.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.05);
|
|
|
|
gainNode.gain.setValueAtTime(0, ctx.currentTime);
|
|
gainNode.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.01);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
|
|
|
|
oscillator.start(ctx.currentTime);
|
|
oscillator.stop(ctx.currentTime + 0.05);
|
|
};
|
|
|
|
// Start tick sound during spinning
|
|
const startTickSound = (duration: number) => {
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
}
|
|
|
|
let tickInterval = 50;
|
|
const maxInterval = 200;
|
|
const intervalIncrement = (maxInterval - tickInterval) / (duration / 100);
|
|
|
|
const tick = () => {
|
|
playTickSound();
|
|
tickInterval += intervalIncrement;
|
|
|
|
if (tickInterval < maxInterval) {
|
|
tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval));
|
|
}
|
|
};
|
|
|
|
tick();
|
|
|
|
setTimeout(() => {
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
tickIntervalRef.current = null;
|
|
}
|
|
}, duration + 100);
|
|
};
|
|
|
|
// Generate random order ID
|
|
const generateRandomOrderId = () => {
|
|
return Math.floor(100000000 + Math.random() * 900000000).toString();
|
|
};
|
|
|
|
// Mock data
|
|
useEffect(() => {
|
|
const mockOrders: Order[] = Array.from({ length: 28 }, (_, i) => ({
|
|
orderId: generateRandomOrderId(),
|
|
customerName: `Customer ${i + 1}`,
|
|
email: `customer${i + 1}@email.com`,
|
|
amount: 150000 + (i * 10000),
|
|
status: 'completed'
|
|
}));
|
|
setOrders(mockOrders);
|
|
setCurrentWheelItems(mockOrders);
|
|
setShuffledDisplayOrder(shuffleArray(mockOrders));
|
|
}, []);
|
|
|
|
const shuffleArray = <T,>(array: T[]): T[] => {
|
|
const shuffled = [...array];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
};
|
|
|
|
const performSpin = (finalWinners: Order[], mainWinner: Order) => {
|
|
const wheelElement = wheelRef.current;
|
|
if (!wheelElement) return;
|
|
|
|
const CARD_HEIGHT = 100;
|
|
const WINNER_ZONE_CENTER = 250;
|
|
|
|
// Find where the winner already exists in current wheel items (no manipulation)
|
|
let winnerIndex = currentWheelItems.findIndex(item => item.orderId === mainWinner.orderId);
|
|
|
|
// If winner not found in wheel, find a suitable position
|
|
if (winnerIndex === -1) {
|
|
winnerIndex = Math.floor(Math.random() * currentWheelItems.length);
|
|
}
|
|
|
|
// Reset wheel position
|
|
wheelElement.style.transition = 'none';
|
|
wheelElement.style.transform = 'translateY(0px)';
|
|
wheelElement.offsetHeight;
|
|
|
|
setTimeout(() => {
|
|
// Calculate spin parameters
|
|
const minSpins = 15;
|
|
const maxSpins = 25;
|
|
const spins = minSpins + Math.random() * (maxSpins - minSpins);
|
|
const totalItems = currentWheelItems.length;
|
|
|
|
// Calculate final position to land on winner
|
|
const totalRotations = Math.floor(spins);
|
|
const baseScrollDistance = totalRotations * totalItems * CARD_HEIGHT;
|
|
const winnerScrollPosition = winnerIndex * CARD_HEIGHT;
|
|
const finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_CENTER - (CARD_HEIGHT / 2);
|
|
|
|
// Dynamic duration
|
|
const distance = Math.abs(finalPosition);
|
|
const baseDuration = 3000;
|
|
const maxDuration = 5000;
|
|
const duration = Math.min(baseDuration + (distance / 10000) * 1000, maxDuration);
|
|
|
|
console.log('🎲 NATURAL SPIN:');
|
|
console.log('Selected Winner:', mainWinner.orderId, '-', mainWinner.customerName);
|
|
console.log('Found at Index:', winnerIndex);
|
|
console.log('Will show in result:', mainWinner.orderId);
|
|
|
|
// Animate wheel
|
|
wheelElement.style.transform = `translateY(${finalPosition}px)`;
|
|
wheelElement.style.transition = `transform ${duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)`;
|
|
|
|
// Start sound effect
|
|
startTickSound(duration);
|
|
|
|
// Complete spinning after animation
|
|
setTimeout(() => {
|
|
// Stop sound
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
tickIntervalRef.current = null;
|
|
}
|
|
|
|
// Set the ACTUAL pre-selected winner (not what's visually in wheel)
|
|
setSelectedWinner(mainWinner);
|
|
|
|
// Add PRE-SELECTED winners to results (guaranteed match)
|
|
const newWinners = finalWinners.map((order, index) => ({
|
|
order,
|
|
timestamp: new Date(),
|
|
position: winners.length + index + 1
|
|
}));
|
|
|
|
setWinners(prev => [...prev, ...newWinners]);
|
|
setIsSpinning(false);
|
|
|
|
console.log('🏆 RESULT WINNER:', newWinners[0].order.orderId);
|
|
console.log('🎯 Visual winner may differ from result (result is guaranteed correct)');
|
|
|
|
// Reset wheel for next spin
|
|
setTimeout(() => {
|
|
wheelElement.style.transition = 'none';
|
|
wheelElement.style.transform = 'translateY(0px)';
|
|
wheelElement.offsetHeight;
|
|
}, 1500);
|
|
|
|
}, duration);
|
|
}, 100);
|
|
};
|
|
|
|
const spinWheel = async () => {
|
|
// Filter orders yang belum menang
|
|
const availableOrders = orders.filter(order =>
|
|
!winners.some(winner => winner.order.orderId === order.orderId)
|
|
);
|
|
|
|
if (availableOrders.length === 0 || isSpinning) return;
|
|
|
|
setIsSpinning(true);
|
|
setSelectedWinner(null);
|
|
|
|
// Reset wheel position before every spin
|
|
const wheelElement = wheelRef.current;
|
|
if (wheelElement) {
|
|
wheelElement.style.transition = 'none';
|
|
wheelElement.style.transform = 'translateY(0px)';
|
|
wheelElement.offsetHeight; // Force reflow
|
|
}
|
|
|
|
// Pilih winner yang akan ditampilkan di result
|
|
const shuffledAvailable = shuffleArray(availableOrders);
|
|
const finalWinners = shuffledAvailable.slice(0, Math.min(numberOfWinners, availableOrders.length));
|
|
const mainWinner = finalWinners[0];
|
|
|
|
console.log('🎯 PRE-SELECTED WINNER:', mainWinner.orderId, '-', mainWinner.customerName);
|
|
console.log('🎯 THIS WINNER WILL BE GUARANTEED IN RESULT');
|
|
|
|
// Always call performSpin with the guaranteed winner
|
|
if (wheelElement) {
|
|
performSpin(finalWinners, mainWinner);
|
|
}
|
|
};
|
|
|
|
const clearResults = () => {
|
|
// Stop any ongoing sound effects
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
tickIntervalRef.current = null;
|
|
}
|
|
|
|
setWinners([]);
|
|
setSelectedWinner(null);
|
|
setIsSpinning(false); // Force reset spinning state
|
|
|
|
// Reset wheel position when clearing
|
|
const wheelElement = wheelRef.current;
|
|
if (wheelElement) {
|
|
wheelElement.style.transition = 'none';
|
|
wheelElement.style.transform = 'translateY(0px)';
|
|
}
|
|
};
|
|
|
|
const refreshOrders = () => {
|
|
// Stop any ongoing sound effects
|
|
if (tickIntervalRef.current) {
|
|
clearTimeout(tickIntervalRef.current);
|
|
tickIntervalRef.current = null;
|
|
}
|
|
|
|
const shuffled = shuffleArray(orders);
|
|
setCurrentWheelItems(shuffled);
|
|
setShuffledDisplayOrder(shuffleArray(orders)); // Shuffle display order too
|
|
setSelectedWinner(null);
|
|
setIsSpinning(false); // Force reset spinning state
|
|
|
|
// Reset wheel position
|
|
const wheelElement = wheelRef.current;
|
|
if (wheelElement) {
|
|
wheelElement.style.transition = 'none';
|
|
wheelElement.style.transform = 'translateY(0px)';
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
minimumFractionDigits: 0
|
|
}).format(amount);
|
|
};
|
|
|
|
// Get available orders for spinning
|
|
const availableOrdersCount = orders.filter(order =>
|
|
!winners.some(winner => winner.order.orderId === order.orderId)
|
|
).length;
|
|
|
|
const GridTable = ({ data, columns = 4 }: { data: Order[], columns?: number }) => {
|
|
const rows = Math.ceil(data.length / columns);
|
|
|
|
return (
|
|
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
|
|
{Array.from({ length: columns }, (_, colIndex) => (
|
|
<div key={colIndex} className="space-y-2">
|
|
{Array.from({ length: rows }, (_, rowIndex) => {
|
|
const itemIndex = rowIndex * columns + colIndex;
|
|
const item = shuffledDisplayOrder[itemIndex];
|
|
|
|
if (!item) return <div key={rowIndex} className="h-16"></div>;
|
|
|
|
const isWinner = winners.some(winner => winner.order.orderId === item.orderId);
|
|
|
|
return (
|
|
<div
|
|
key={item.orderId + rowIndex}
|
|
className={`border-2 rounded-lg p-3 text-center min-h-16 flex items-center justify-center ${
|
|
isWinner
|
|
? 'border-yellow-500 bg-yellow-50 text-yellow-700 font-bold'
|
|
: 'border-primary bg-white text-primary'
|
|
}`}
|
|
>
|
|
<div className="text-lg font-semibold">
|
|
{item.orderId}
|
|
{isWinner && (
|
|
<Trophy className="inline ml-2 text-yellow-500" size={16} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderTabContent = () => {
|
|
switch (activeTab) {
|
|
case 'acak':
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Controls */}
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-xl font-semibold flex items-center gap-2 text-gray-900">
|
|
<Database size={20} className="text-primary" />
|
|
Order Database ({orders.length} total, {availableOrdersCount} available)
|
|
</h2>
|
|
<button
|
|
onClick={refreshOrders}
|
|
className="p-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-300"
|
|
title="Shuffle Orders Position"
|
|
disabled={isSpinning}
|
|
>
|
|
<RefreshCw size={16} className="text-gray-700" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Number of winners
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max={availableOrdersCount}
|
|
value={numberOfWinners}
|
|
onChange={(e) => setNumberOfWinners(Math.min(parseInt(e.target.value) || 1, availableOrdersCount))}
|
|
className="w-32 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"
|
|
disabled={isSpinning}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={spinWheel}
|
|
disabled={availableOrdersCount === 0 || isSpinning}
|
|
className="w-full bg-primary hover:bg-primary/90 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold py-4 px-6 rounded-lg flex items-center justify-center gap-3 transition-colors text-lg shadow-lg"
|
|
>
|
|
<RotateCw size={24} className={isSpinning ? 'animate-spin' : ''} />
|
|
{isSpinning ? `Spinning...` : availableOrdersCount === 0 ? 'No Available Orders' : 'Spin the Wheel'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Grid Table */}
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<div className="mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Eligible Orders</h3>
|
|
</div>
|
|
<GridTable data={orders} columns={4} />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'hadiah':
|
|
return (
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Prize Management</h3>
|
|
<p className="text-gray-600">Prize configuration will be available here.</p>
|
|
</div>
|
|
);
|
|
|
|
case 'winner':
|
|
return (
|
|
<div className="space-y-6">
|
|
{winners.length > 0 ? (
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-2xl font-semibold flex items-center gap-2 text-gray-900">
|
|
<Trophy className="text-yellow-500" size={28} />
|
|
Draw Results ({winners.length} Winners)
|
|
</h3>
|
|
<button
|
|
onClick={clearResults}
|
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
|
disabled={isSpinning}
|
|
>
|
|
<Trash2 size={16} />
|
|
Clear Results
|
|
</button>
|
|
</div>
|
|
|
|
{/* Simple Winner List */}
|
|
<div className="space-y-3">
|
|
{winners.map((winner) => (
|
|
<div
|
|
key={`result-${winner.order.orderId}-${winner.timestamp.getTime()}`}
|
|
className="flex items-center justify-between bg-gray-50 p-4 rounded-lg border border-gray-200"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="bg-yellow-500 text-white px-3 py-1 rounded-full text-sm font-bold">
|
|
#{winner.position}
|
|
</div>
|
|
<div>
|
|
<div className="font-bold text-lg text-gray-900">{winner.order.customerName}</div>
|
|
<div className="text-gray-600 text-sm">{winner.order.orderId} • {winner.order.email}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<div className="text-primary font-bold text-lg">{formatCurrency(winner.order.amount)}</div>
|
|
<div className="text-gray-500 text-xs">Won at: {winner.timestamp.toLocaleTimeString()}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<div className="text-center py-8">
|
|
<Trophy className="mx-auto text-gray-400 mb-4" size={48} />
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Winners Yet</h3>
|
|
<p className="text-gray-600">Start spinning to see the winners here!</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white text-gray-900 p-6">
|
|
<div className="max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-4xl font-bold text-primary mb-2">Random Order Draw</h1>
|
|
<p className="text-gray-600">Select random winners from order database</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="mb-6">
|
|
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg w-fit mx-auto">
|
|
{[
|
|
{ id: 'acak', label: 'acak' },
|
|
{ id: 'hadiah', label: 'hadiah' },
|
|
{ id: 'winner', label: 'winner' }
|
|
].map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as any)}
|
|
className={`px-6 py-2 rounded-md font-medium transition-colors ${
|
|
activeTab === tab.id
|
|
? 'bg-white text-primary shadow-sm border-2 border-primary'
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
|
|
{/* Left Panel - Tab Content */}
|
|
<div className="space-y-6">
|
|
{renderTabContent()}
|
|
</div>
|
|
|
|
{/* Right Panel - Spin Wheel */}
|
|
<div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
|
|
<h3 className="text-xl font-semibold mb-4 text-center text-gray-900">Spin Wheel</h3>
|
|
|
|
{/* Wheel Container */}
|
|
<div
|
|
className="relative w-full bg-gray-100 rounded-lg overflow-hidden border-2 border-primary"
|
|
style={{ height: '500px' }}
|
|
>
|
|
{/* Winner Selection Zone */}
|
|
<div
|
|
className="absolute left-0 right-0 z-50 border-2 border-red-500 bg-red-500/15"
|
|
style={{
|
|
top: '200px',
|
|
height: '100px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
}}
|
|
>
|
|
{/* Left Arrow */}
|
|
<div
|
|
className="absolute w-0 h-0 border-t-transparent border-b-transparent border-r-red-500"
|
|
style={{
|
|
left: '-20px',
|
|
borderTopWidth: '20px',
|
|
borderBottomWidth: '20px',
|
|
borderRightWidth: '25px'
|
|
}}
|
|
></div>
|
|
|
|
{/* Winner Badge */}
|
|
<div className="bg-red-600 text-white px-6 py-3 rounded-lg font-bold text-lg tracking-wider shadow-lg">
|
|
🎯 WINNER 🎯
|
|
</div>
|
|
|
|
{/* Right Arrow */}
|
|
<div
|
|
className="absolute w-0 h-0 border-t-transparent border-b-transparent border-l-red-500"
|
|
style={{
|
|
right: '-20px',
|
|
borderTopWidth: '20px',
|
|
borderBottomWidth: '20px',
|
|
borderLeftWidth: '25px'
|
|
}}
|
|
></div>
|
|
</div>
|
|
|
|
{/* Scrolling Cards */}
|
|
<div
|
|
ref={wheelRef}
|
|
className="absolute inset-0"
|
|
style={{ transform: 'translateY(0px)' }}
|
|
>
|
|
{/* Generate enough cards for smooth scrolling */}
|
|
{Array.from({ length: 40 }, (_, repeatIndex) =>
|
|
currentWheelItems.map((order, orderIndex) => {
|
|
const isCurrentWinner = winners.some(winner => winner.order.orderId === order.orderId);
|
|
const isSelectedWinner = selectedWinner?.orderId === order.orderId;
|
|
|
|
return (
|
|
<div
|
|
key={`repeat-${repeatIndex}-order-${orderIndex}`}
|
|
className={`flex items-center px-4 border-b border-gray-300 ${
|
|
isCurrentWinner ? 'bg-primary/10' :
|
|
isSelectedWinner ? 'bg-yellow-100' :
|
|
orderIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'
|
|
}`}
|
|
style={{
|
|
height: '100px',
|
|
minHeight: '100px',
|
|
maxHeight: '100px'
|
|
}}
|
|
>
|
|
{/* Order Info */}
|
|
<div className="flex-1">
|
|
<div className={`font-bold text-xl mb-1 ${
|
|
isCurrentWinner ? 'text-primary' :
|
|
isSelectedWinner ? 'text-yellow-600' :
|
|
'text-gray-900'
|
|
}`}>
|
|
{order.orderId}
|
|
</div>
|
|
<div className={`text-base ${
|
|
isCurrentWinner ? 'text-primary/80' :
|
|
isSelectedWinner ? 'text-yellow-700' :
|
|
'text-gray-600'
|
|
}`}>
|
|
{order.customerName}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Amount */}
|
|
<div className="text-right">
|
|
<div className="text-primary font-bold text-lg">
|
|
{formatCurrency(order.amount)}
|
|
</div>
|
|
{(isCurrentWinner || isSelectedWinner) && (
|
|
<Trophy className="inline ml-2 text-yellow-500" size={16} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* Gradient Overlays */}
|
|
<div className="absolute top-0 left-0 right-0 h-48 bg-gradient-to-b from-gray-100 via-gray-100/70 to-transparent pointer-events-none z-40"></div>
|
|
<div className="absolute bottom-0 left-0 right-0 h-48 bg-gradient-to-t from-gray-100 via-gray-100/70 to-transparent pointer-events-none z-40"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RandomDrawApp;
|