Merge pull request 'Update draw' (#10) from efril into main

Reviewed-on: #10
This commit is contained in:
aefril 2025-09-13 08:21:07 +00:00
commit 5de78cb78f

View File

@ -1,372 +1,376 @@
'use client' 'use client';
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react';
import { Users, RotateCw, Trophy, Trash2, RefreshCw, Database } from 'lucide-react' import { Users, RotateCw, Trophy, Trash2, RefreshCw, Database } from 'lucide-react';
interface Order { interface Order {
orderId: string orderId: string;
customerName: string customerName: string;
email: string email: string;
amount: number amount: number;
status: string status: string;
} }
interface WinnerResult { interface WinnerResult {
order: Order order: Order;
timestamp: Date timestamp: Date;
position: number position: number;
} }
const RandomDrawApp: React.FC = () => { const RandomDrawApp: React.FC = () => {
const [orders, setOrders] = useState<Order[]>([]) const [orders, setOrders] = useState<Order[]>([]);
const [isSpinning, setIsSpinning] = useState<boolean>(false) const [isSpinning, setIsSpinning] = useState<boolean>(false);
const [winners, setWinners] = useState<WinnerResult[]>([]) const [winners, setWinners] = useState<WinnerResult[]>([]);
const [numberOfWinners, setNumberOfWinners] = useState<number>(1) const [numberOfWinners, setNumberOfWinners] = useState<number>(1);
const [currentWheelItems, setCurrentWheelItems] = useState<Order[]>([]) const [currentWheelItems, setCurrentWheelItems] = useState<Order[]>([]);
const [selectedWinner, setSelectedWinner] = useState<Order | null>(null) const [selectedWinner, setSelectedWinner] = useState<Order | null>(null);
const wheelRef = useRef<HTMLDivElement>(null) const [activeTab, setActiveTab] = useState<'acak' | 'hadiah' | 'winner'>('acak');
const audioContextRef = useRef<AudioContext | null>(null) const [shuffledDisplayOrder, setShuffledDisplayOrder] = useState<Order[]>([]);
const tickIntervalRef = useRef<NodeJS.Timeout | null>(null) const wheelRef = useRef<HTMLDivElement>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const tickIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Initialize Audio Context // Initialize Audio Context
useEffect(() => { useEffect(() => {
const initAudio = () => { const initAudio = () => {
if (!audioContextRef.current) { if (!audioContextRef.current) {
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);
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
} }
} };
}, []) }, []);
// 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);
} };
// Start tick sound during spinning // Start tick sound during spinning
const startTickSound = (duration: number) => { const startTickSound = (duration: number) => {
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
} }
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) {
tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval)) tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval));
}
} }
};
tick() tick();
setTimeout(() => { setTimeout(() => {
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
tickIntervalRef.current = null tickIntervalRef.current = null;
}
}, duration + 100)
} }
}, duration + 100);
};
// Generate random order ID
const generateRandomOrderId = () => {
return Math.floor(100000000 + Math.random() * 900000000).toString();
};
// Mock data // Mock data
useEffect(() => { useEffect(() => {
const mockOrders: Order[] = [ const mockOrders: Order[] = Array.from({ length: 28 }, (_, i) => ({
{ orderId: 'ORD-001', customerName: 'John Doe', email: 'john@email.com', amount: 150000, status: 'completed' }, orderId: generateRandomOrderId(),
{ orderId: 'ORD-002', customerName: 'Jane Smith', email: 'jane@email.com', amount: 250000, status: 'completed' }, customerName: `Customer ${i + 1}`,
{ orderId: 'ORD-003', customerName: 'Bob Johnson', email: 'bob@email.com', amount: 180000, status: 'completed' }, email: `customer${i + 1}@email.com`,
{ amount: 150000 + (i * 10000),
orderId: 'ORD-004',
customerName: 'Alice Brown',
email: 'alice@email.com',
amount: 320000,
status: 'completed' status: 'completed'
}, }));
{ setOrders(mockOrders);
orderId: 'ORD-005', setCurrentWheelItems(mockOrders);
customerName: 'Charlie Wilson', setShuffledDisplayOrder(shuffleArray(mockOrders));
email: 'charlie@email.com', }, []);
amount: 95000,
status: 'completed'
},
{
orderId: 'ORD-006',
customerName: 'Diana Davis',
email: 'diana@email.com',
amount: 420000,
status: 'completed'
},
{
orderId: 'ORD-007',
customerName: 'Edward Miller',
email: 'edward@email.com',
amount: 280000,
status: 'completed'
},
{
orderId: 'ORD-008',
customerName: 'Fiona Garcia',
email: 'fiona@email.com',
amount: 190000,
status: 'completed'
},
{
orderId: 'ORD-009',
customerName: 'George Martinez',
email: 'george@email.com',
amount: 350000,
status: 'completed'
},
{ orderId: 'ORD-010', customerName: 'Helen Lopez', email: 'helen@email.com', amount: 210000, status: 'completed' }
]
setOrders(mockOrders)
setCurrentWheelItems(mockOrders)
}, [])
const shuffleArray = <T,>(array: T[]): T[] => { const shuffleArray = <T,>(array: T[]): T[] => {
const shuffled = [...array] const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) { for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)) const j = Math.floor(Math.random() * (i + 1));
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]] [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled
} }
return shuffled;
};
const performSpin = (finalWinners: Order[], mainWinner: Order) => { const performSpin = (finalWinners: Order[], mainWinner: Order) => {
const wheelElement = wheelRef.current const wheelElement = wheelRef.current;
if (!wheelElement) return if (!wheelElement) return;
const CARD_HEIGHT = 100 const CARD_HEIGHT = 100;
const WINNER_ZONE_CENTER = 250 const WINNER_ZONE_CENTER = 250;
// Find where the winner already exists in current wheel items (no manipulation) // Find where the winner already exists in current wheel items (no manipulation)
let winnerIndex = currentWheelItems.findIndex(item => item.orderId === mainWinner.orderId) let winnerIndex = currentWheelItems.findIndex(item => item.orderId === mainWinner.orderId);
// If winner not found in wheel, find a suitable position // If winner not found in wheel, find a suitable position
if (winnerIndex === -1) { if (winnerIndex === -1) {
winnerIndex = Math.floor(Math.random() * currentWheelItems.length) winnerIndex = Math.floor(Math.random() * currentWheelItems.length);
} }
// Reset wheel position // Reset wheel position
wheelElement.style.transition = 'none' wheelElement.style.transition = 'none';
wheelElement.style.transform = 'translateY(0px)' wheelElement.style.transform = 'translateY(0px)';
wheelElement.offsetHeight wheelElement.offsetHeight;
setTimeout(() => { setTimeout(() => {
// Calculate spin parameters // Calculate spin parameters
const minSpins = 15 const minSpins = 15;
const maxSpins = 25 const maxSpins = 25;
const spins = minSpins + Math.random() * (maxSpins - minSpins) const spins = minSpins + Math.random() * (maxSpins - minSpins);
const totalItems = currentWheelItems.length const totalItems = currentWheelItems.length;
// Calculate final position to land on winner // Calculate final position to land on winner
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_CENTER - CARD_HEIGHT / 2 const finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_CENTER - (CARD_HEIGHT / 2);
// Dynamic duration // Dynamic duration
const distance = Math.abs(finalPosition) const distance = Math.abs(finalPosition);
const baseDuration = 3000 const baseDuration = 3000;
const maxDuration = 5000 const maxDuration = 5000;
const duration = Math.min(baseDuration + (distance / 10000) * 1000, maxDuration) const duration = Math.min(baseDuration + (distance / 10000) * 1000, maxDuration);
console.log('🎲 NATURAL SPIN:') console.log('🎲 NATURAL SPIN:');
console.log('Selected Winner:', mainWinner.orderId, '-', mainWinner.customerName) console.log('Selected Winner:', mainWinner.orderId, '-', mainWinner.customerName);
console.log('Found at Index:', winnerIndex) console.log('Found at Index:', winnerIndex);
console.log('Will show in result:', mainWinner.orderId) console.log('Will show in result:', mainWinner.orderId);
// Animate wheel // Animate wheel
wheelElement.style.transform = `translateY(${finalPosition}px)` wheelElement.style.transform = `translateY(${finalPosition}px)`;
wheelElement.style.transition = `transform ${duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)` wheelElement.style.transition = `transform ${duration}ms cubic-bezier(0.17, 0.67, 0.12, 0.99)`;
// Start sound effect // Start sound effect
startTickSound(duration) startTickSound(duration);
// Complete spinning after animation // Complete spinning after animation
setTimeout(() => { setTimeout(() => {
// Stop sound // Stop sound
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
tickIntervalRef.current = null tickIntervalRef.current = null;
} }
// Set the ACTUAL pre-selected winner (not what's visually in wheel) // Set the ACTUAL pre-selected winner (not what's visually in wheel)
setSelectedWinner(mainWinner) setSelectedWinner(mainWinner);
// Add PRE-SELECTED winners to results (guaranteed match) // Add PRE-SELECTED winners to results (guaranteed match)
const newWinners = finalWinners.map((order, index) => ({ const newWinners = finalWinners.map((order, index) => ({
order, order,
timestamp: new Date(), timestamp: new Date(),
position: winners.length + index + 1 position: winners.length + index + 1
})) }));
setWinners(prev => [...prev, ...newWinners]) setWinners(prev => [...prev, ...newWinners]);
setIsSpinning(false) setIsSpinning(false);
console.log('🏆 RESULT WINNER:', newWinners[0].order.orderId) console.log('🏆 RESULT WINNER:', newWinners[0].order.orderId);
console.log('🎯 Visual winner may differ from result (result is guaranteed correct)') console.log('🎯 Visual winner may differ from result (result is guaranteed correct)');
// Reset wheel for next spin // Reset wheel for next spin
setTimeout(() => { setTimeout(() => {
wheelElement.style.transition = 'none' wheelElement.style.transition = 'none';
wheelElement.style.transform = 'translateY(0px)' wheelElement.style.transform = 'translateY(0px)';
wheelElement.offsetHeight wheelElement.offsetHeight;
}, 1500) }, 1500);
}, duration)
}, 100) }, duration);
} }, 100);
};
const spinWheel = async () => { const spinWheel = async () => {
// Filter orders yang belum menang // Filter orders yang belum menang
const availableOrders = orders.filter(order => !winners.some(winner => winner.order.orderId === order.orderId)) const availableOrders = orders.filter(order =>
!winners.some(winner => winner.order.orderId === order.orderId)
);
if (availableOrders.length === 0 || isSpinning) return if (availableOrders.length === 0 || isSpinning) return;
setIsSpinning(true) setIsSpinning(true);
setSelectedWinner(null) setSelectedWinner(null);
// Reset wheel position before every spin // Reset wheel position before every spin
const wheelElement = wheelRef.current const wheelElement = 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 // Force reflow wheelElement.offsetHeight; // Force reflow
} }
// Pilih winner yang akan ditampilkan di result // Pilih winner yang akan ditampilkan di result
const shuffledAvailable = shuffleArray(availableOrders) const shuffledAvailable = shuffleArray(availableOrders);
const finalWinners = shuffledAvailable.slice(0, Math.min(numberOfWinners, availableOrders.length)) const finalWinners = shuffledAvailable.slice(0, Math.min(numberOfWinners, availableOrders.length));
const mainWinner = finalWinners[0] const mainWinner = finalWinners[0];
console.log('🎯 PRE-SELECTED WINNER:', mainWinner.orderId, '-', mainWinner.customerName) console.log('🎯 PRE-SELECTED WINNER:', mainWinner.orderId, '-', mainWinner.customerName);
console.log('🎯 THIS WINNER WILL BE GUARANTEED IN RESULT') console.log('🎯 THIS WINNER WILL BE GUARANTEED IN RESULT');
// Always call performSpin with the guaranteed winner // Always call performSpin with the guaranteed winner
if (wheelElement) { if (wheelElement) {
performSpin(finalWinners, mainWinner) performSpin(finalWinners, mainWinner);
}
} }
};
const clearResults = () => { const clearResults = () => {
// Stop any ongoing sound effects // Stop any ongoing sound effects
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
tickIntervalRef.current = null tickIntervalRef.current = null;
} }
setWinners([]) setWinners([]);
setSelectedWinner(null) setSelectedWinner(null);
setIsSpinning(false) // Force reset spinning state setIsSpinning(false); // Force reset spinning state
// Reset wheel position when clearing // Reset wheel position when clearing
const wheelElement = wheelRef.current const wheelElement = wheelRef.current;
if (wheelElement) { if (wheelElement) {
wheelElement.style.transition = 'none' wheelElement.style.transition = 'none';
wheelElement.style.transform = 'translateY(0px)' wheelElement.style.transform = 'translateY(0px)';
}
} }
};
const refreshOrders = () => { const refreshOrders = () => {
// Stop any ongoing sound effects // Stop any ongoing sound effects
if (tickIntervalRef.current) { if (tickIntervalRef.current) {
clearTimeout(tickIntervalRef.current) clearTimeout(tickIntervalRef.current);
tickIntervalRef.current = null tickIntervalRef.current = null;
} }
const shuffled = shuffleArray(orders) const shuffled = shuffleArray(orders);
setCurrentWheelItems(shuffled) setCurrentWheelItems(shuffled);
setSelectedWinner(null) setShuffledDisplayOrder(shuffleArray(orders)); // Shuffle display order too
setIsSpinning(false) // Force reset spinning state setSelectedWinner(null);
setIsSpinning(false); // Force reset spinning state
// Reset wheel position // Reset wheel position
const wheelElement = wheelRef.current const wheelElement = wheelRef.current;
if (wheelElement) { if (wheelElement) {
wheelElement.style.transition = 'none' wheelElement.style.transition = 'none';
wheelElement.style.transform = 'translateY(0px)' wheelElement.style.transform = 'translateY(0px)';
}
} }
};
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
minimumFractionDigits: 0 minimumFractionDigits: 0
}).format(amount) }).format(amount);
} };
// Get available orders for spinning // Get available orders for spinning
const availableOrdersCount = orders.filter( const availableOrdersCount = orders.filter(order =>
order => !winners.some(winner => winner.order.orderId === order.orderId) !winners.some(winner => winner.order.orderId === order.orderId)
).length ).length;
const GridTable = ({ data, columns = 4 }: { data: Order[], columns?: number }) => {
const rows = Math.ceil(data.length / columns);
return ( return (
<div className='min-h-screen bg-white text-gray-900 p-6'> <div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
<div className='max-w-7xl mx-auto'> {Array.from({ length: columns }, (_, colIndex) => (
{/* Header */} <div key={colIndex} className="space-y-2">
<div className='text-center mb-8'> {Array.from({ length: rows }, (_, rowIndex) => {
<h1 className='text-4xl font-bold text-primary mb-2'>Random Order Draw</h1> const itemIndex = rowIndex * columns + colIndex;
<p className='text-gray-600'>Select random winners from order database</p> const item = shuffledDisplayOrder[itemIndex];
</div>
{/* Main Layout */} if (!item) return <div key={rowIndex} className="h-16"></div>;
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8'>
{/* Left Panel - Table & Controls */} const isWinner = winners.some(winner => winner.order.orderId === item.orderId);
<div className='space-y-6'>
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 */} {/* Controls */}
<div className='bg-white rounded-lg p-6 border border-gray-200 shadow-sm'> <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
<div className='flex items-center justify-between mb-4'> <div className="flex items-center justify-between mb-4">
<h2 className='text-xl font-semibold flex items-center gap-2 text-gray-900'> <h2 className="text-xl font-semibold flex items-center gap-2 text-gray-900">
<Database size={20} className='text-primary' /> <Database size={20} className="text-primary" />
Order Database ({orders.length} total, {availableOrdersCount} available) Order Database ({orders.length} total, {availableOrdersCount} available)
</h2> </h2>
<button <button
onClick={refreshOrders} onClick={refreshOrders}
className='p-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-300' className="p-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors border border-gray-300"
title='Shuffle Orders' title="Shuffle Orders Position"
disabled={isSpinning} disabled={isSpinning}
> >
<RefreshCw size={16} className='text-gray-700' /> <RefreshCw size={16} className="text-gray-700" />
</button> </button>
</div> </div>
<div className='mb-4'> <div className="mb-4">
<label className='block text-sm font-medium text-gray-700 mb-2'>Number of winners</label> <label className="block text-sm font-medium text-gray-700 mb-2">
Number of winners
</label>
<input <input
type='number' type="number"
min='1' min="1"
max={availableOrdersCount} max={availableOrdersCount}
value={numberOfWinners} value={numberOfWinners}
onChange={e => setNumberOfWinners(Math.min(parseInt(e.target.value) || 1, availableOrdersCount))} 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' 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} disabled={isSpinning}
/> />
</div> </div>
@ -374,75 +378,145 @@ const RandomDrawApp: React.FC = () => {
<button <button
onClick={spinWheel} onClick={spinWheel}
disabled={availableOrdersCount === 0 || isSpinning} 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' 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' : ''} /> <RotateCw size={24} className={isSpinning ? 'animate-spin' : ''} />
{isSpinning ? `Spinning...` : availableOrdersCount === 0 ? 'No Available Orders' : 'Spin the Wheel'} {isSpinning ? `Spinning...` : availableOrdersCount === 0 ? 'No Available Orders' : 'Spin the Wheel'}
</button> </button>
</div> </div>
{/* Orders Table */} {/* Grid Table */}
<div className='bg-white rounded-lg overflow-hidden border border-gray-200 shadow-sm'> <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
<div className='p-4 border-b border-gray-200 bg-gray-50'> <div className="mb-4">
<h3 className='text-lg font-semibold text-gray-900'>Eligible Orders</h3> <h3 className="text-lg font-semibold text-gray-900">Eligible Orders</h3>
</div> </div>
<div className='max-h-96 overflow-y-auto'> <GridTable data={orders} columns={4} />
<table className='w-full'> </div>
<thead className='bg-gray-100 sticky top-0'> </div>
<tr> );
<th className='text-left p-3 text-sm font-medium text-gray-700'>Order ID</th>
<th className='text-left p-3 text-sm font-medium text-gray-700'>Customer</th> case 'hadiah':
<th className='text-left p-3 text-sm font-medium text-gray-700'>Amount</th>
<th className='text-left p-3 text-sm font-medium text-gray-700'>Status</th>
</tr>
</thead>
<tbody>
{orders.map(order => {
const isWinner = winners.some(winner => winner.order.orderId === order.orderId)
return ( return (
<tr <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
key={order.orderId} <h3 className="text-lg font-semibold text-gray-900 mb-4">Prize Management</h3>
className={`border-b border-gray-100 hover:bg-gray-50 transition-colors ${ <p className="text-gray-600">Prize configuration will be available here.</p>
isWinner ? 'bg-primary/10' : '' </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}
> >
<td className='p-3 text-sm font-mono text-gray-900'>{order.orderId}</td> <Trash2 size={16} />
<td className='p-3 text-sm'> Clear Results
<div className='text-gray-900'>{order.customerName}</div> </button>
<div className='text-gray-500 text-xs'>{order.email}</div> </div>
</td>
<td className='p-3 text-sm text-gray-900'>{formatCurrency(order.amount)}</td> {/* Simple Winner List */}
<td className='p-3 text-sm'> <div className="space-y-3">
<span {winners.map((winner) => (
className={`px-2 py-1 rounded-full text-xs text-white ${ <div
order.status === 'completed' ? 'bg-primary' : 'bg-yellow-500' 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"
> >
{order.status} <div className="flex items-center gap-4">
</span> <div className="bg-yellow-500 text-white px-3 py-1 rounded-full text-sm font-bold">
{isWinner && <Trophy className='inline ml-2 text-yellow-500' size={16} />} #{winner.position}
</td> </div>
</tr> <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>
</tbody>
</table>
</div> </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> </div>
{/* Right Panel - Spin Wheel */} {/* Right Panel - Spin Wheel */}
<div className='bg-white rounded-lg p-6 border border-gray-200 shadow-sm'> <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> <h3 className="text-xl font-semibold mb-4 text-center text-gray-900">Spin Wheel</h3>
{/* Wheel Container */} {/* Wheel Container */}
<div <div
className='relative w-full bg-gray-100 rounded-lg overflow-hidden border-2 border-primary' className="relative w-full bg-gray-100 rounded-lg overflow-hidden border-2 border-primary"
style={{ height: '500px' }} style={{ height: '500px' }}
> >
{/* Winner Selection Zone */} {/* Winner Selection Zone */}
<div <div
className='absolute left-0 right-0 z-50 border-2 border-red-500 bg-red-500/15' className="absolute left-0 right-0 z-50 border-2 border-red-500 bg-red-500/15"
style={{ style={{
top: '200px', top: '200px',
height: '100px', height: '100px',
@ -453,7 +527,7 @@ const RandomDrawApp: React.FC = () => {
> >
{/* Left Arrow */} {/* Left Arrow */}
<div <div
className='absolute w-0 h-0 border-t-transparent border-b-transparent border-r-red-500' className="absolute w-0 h-0 border-t-transparent border-b-transparent border-r-red-500"
style={{ style={{
left: '-20px', left: '-20px',
borderTopWidth: '20px', borderTopWidth: '20px',
@ -463,13 +537,13 @@ const RandomDrawApp: React.FC = () => {
></div> ></div>
{/* Winner Badge */} {/* Winner Badge */}
<div className='bg-red-600 text-white px-6 py-3 rounded-lg font-bold text-lg tracking-wider shadow-lg'> <div className="bg-red-600 text-white px-6 py-3 rounded-lg font-bold text-lg tracking-wider shadow-lg">
🎯 WINNER 🎯 🎯 WINNER 🎯
</div> </div>
{/* Right Arrow */} {/* Right Arrow */}
<div <div
className='absolute w-0 h-0 border-t-transparent border-b-transparent border-l-red-500' className="absolute w-0 h-0 border-t-transparent border-b-transparent border-l-red-500"
style={{ style={{
right: '-20px', right: '-20px',
borderTopWidth: '20px', borderTopWidth: '20px',
@ -480,24 +554,24 @@ const RandomDrawApp: React.FC = () => {
</div> </div>
{/* Scrolling Cards */} {/* Scrolling Cards */}
<div ref={wheelRef} className='absolute inset-0' style={{ transform: 'translateY(0px)' }}> <div
ref={wheelRef}
className="absolute inset-0"
style={{ transform: 'translateY(0px)' }}
>
{/* Generate enough cards for smooth scrolling */} {/* Generate enough cards for smooth scrolling */}
{Array.from({ length: 40 }, (_, repeatIndex) => {Array.from({ length: 40 }, (_, repeatIndex) =>
currentWheelItems.map((order, orderIndex) => { currentWheelItems.map((order, orderIndex) => {
const isCurrentWinner = winners.some(winner => winner.order.orderId === order.orderId) const isCurrentWinner = winners.some(winner => winner.order.orderId === order.orderId);
const isSelectedWinner = selectedWinner?.orderId === order.orderId const isSelectedWinner = selectedWinner?.orderId === order.orderId;
return ( return (
<div <div
key={`repeat-${repeatIndex}-order-${orderIndex}`} key={`repeat-${repeatIndex}-order-${orderIndex}`}
className={`flex items-center px-4 border-b border-gray-300 ${ className={`flex items-center px-4 border-b border-gray-300 ${
isCurrentWinner isCurrentWinner ? 'bg-primary/10' :
? 'bg-primary/10' isSelectedWinner ? 'bg-yellow-100' :
: isSelectedWinner orderIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'
? 'bg-yellow-100'
: orderIndex % 2 === 0
? 'bg-white'
: 'bg-gray-50'
}`} }`}
style={{ style={{
height: '100px', height: '100px',
@ -506,96 +580,47 @@ const RandomDrawApp: React.FC = () => {
}} }}
> >
{/* Order Info */} {/* Order Info */}
<div className='flex-1'> <div className="flex-1">
<div <div className={`font-bold text-xl mb-1 ${
className={`font-bold text-xl mb-1 ${ isCurrentWinner ? 'text-primary' :
isCurrentWinner ? 'text-primary' : isSelectedWinner ? 'text-yellow-600' : 'text-gray-900' isSelectedWinner ? 'text-yellow-600' :
}`} 'text-gray-900'
> }`}>
{order.orderId} {order.orderId}
</div> </div>
<div <div className={`text-base ${
className={`text-base ${ isCurrentWinner ? 'text-primary/80' :
isCurrentWinner isSelectedWinner ? 'text-yellow-700' :
? 'text-primary/80' 'text-gray-600'
: isSelectedWinner }`}>
? 'text-yellow-700'
: 'text-gray-600'
}`}
>
{order.customerName} {order.customerName}
</div> </div>
</div> </div>
{/* Amount */} {/* Amount */}
<div className='text-right'> <div className="text-right">
<div className='text-primary font-bold text-lg'>{formatCurrency(order.amount)}</div> <div className="text-primary font-bold text-lg">
{formatCurrency(order.amount)}
</div>
{(isCurrentWinner || isSelectedWinner) && ( {(isCurrentWinner || isSelectedWinner) && (
<Trophy className='inline ml-2 text-yellow-500' size={16} /> <Trophy className="inline ml-2 text-yellow-500" size={16} />
)} )}
</div> </div>
</div> </div>
) );
}) })
)} )}
</div> </div>
{/* Gradient Overlays */} {/* 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 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 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> </div>
</div>
</div>
);
};
{/* Results Section */} export default RandomDrawApp;
{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>
</div>
)
}
export default RandomDrawApp