Compare commits
No commits in common. "f3358705b0d83bdef1331b26814ec8f234fb5f0e" and "14efada9bb1259f373cefc39a98e4309a80afb5f" have entirely different histories.
f3358705b0
...
14efada9bb
10
package-lock.json
generated
10
package-lock.json
generated
@ -55,7 +55,6 @@
|
|||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"keen-slider": "6.8.6",
|
"keen-slider": "6.8.6",
|
||||||
"lucide-react": "^0.544.0",
|
|
||||||
"mapbox-gl": "3.9.0",
|
"mapbox-gl": "3.9.0",
|
||||||
"negotiator": "1.0.0",
|
"negotiator": "1.0.0",
|
||||||
"next": "15.1.2",
|
"next": "15.1.2",
|
||||||
@ -8463,15 +8462,6 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
|
||||||
"version": "0.544.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
|
|
||||||
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mapbox-gl": {
|
"node_modules/mapbox-gl": {
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.9.0.tgz",
|
||||||
|
|||||||
@ -61,7 +61,6 @@
|
|||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"keen-slider": "6.8.6",
|
"keen-slider": "6.8.6",
|
||||||
"lucide-react": "^0.544.0",
|
|
||||||
"mapbox-gl": "3.9.0",
|
"mapbox-gl": "3.9.0",
|
||||||
"negotiator": "1.0.0",
|
"negotiator": "1.0.0",
|
||||||
"next": "15.1.2",
|
"next": "15.1.2",
|
||||||
|
|||||||
@ -1,601 +0,0 @@
|
|||||||
'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 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data
|
|
||||||
useEffect(() => {
|
|
||||||
const mockOrders: Order[] = [
|
|
||||||
{ orderId: 'ORD-001', customerName: 'John Doe', email: 'john@email.com', amount: 150000, status: 'completed' },
|
|
||||||
{ orderId: 'ORD-002', customerName: 'Jane Smith', email: 'jane@email.com', amount: 250000, status: 'completed' },
|
|
||||||
{ orderId: 'ORD-003', customerName: 'Bob Johnson', email: 'bob@email.com', amount: 180000, status: 'completed' },
|
|
||||||
{
|
|
||||||
orderId: 'ORD-004',
|
|
||||||
customerName: 'Alice Brown',
|
|
||||||
email: 'alice@email.com',
|
|
||||||
amount: 320000,
|
|
||||||
status: 'completed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
orderId: 'ORD-005',
|
|
||||||
customerName: 'Charlie Wilson',
|
|
||||||
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 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)
|
|
||||||
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
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Main Layout */}
|
|
||||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8'>
|
|
||||||
{/* Left Panel - Table & Controls */}
|
|
||||||
<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'
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Orders Table */}
|
|
||||||
<div className='bg-white rounded-lg overflow-hidden border border-gray-200 shadow-sm'>
|
|
||||||
<div className='p-4 border-b border-gray-200 bg-gray-50'>
|
|
||||||
<h3 className='text-lg font-semibold text-gray-900'>Eligible Orders</h3>
|
|
||||||
</div>
|
|
||||||
<div className='max-h-96 overflow-y-auto'>
|
|
||||||
<table className='w-full'>
|
|
||||||
<thead className='bg-gray-100 sticky top-0'>
|
|
||||||
<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>
|
|
||||||
<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 (
|
|
||||||
<tr
|
|
||||||
key={order.orderId}
|
|
||||||
className={`border-b border-gray-100 hover:bg-gray-50 transition-colors ${
|
|
||||||
isWinner ? 'bg-primary/10' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<td className='p-3 text-sm font-mono text-gray-900'>{order.orderId}</td>
|
|
||||||
<td className='p-3 text-sm'>
|
|
||||||
<div className='text-gray-900'>{order.customerName}</div>
|
|
||||||
<div className='text-gray-500 text-xs'>{order.email}</div>
|
|
||||||
</td>
|
|
||||||
<td className='p-3 text-sm text-gray-900'>{formatCurrency(order.amount)}</td>
|
|
||||||
<td className='p-3 text-sm'>
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs text-white ${
|
|
||||||
order.status === 'completed' ? 'bg-primary' : 'bg-yellow-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{order.status}
|
|
||||||
</span>
|
|
||||||
{isWinner && <Trophy className='inline ml-2 text-yellow-500' size={16} />}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Results Section */}
|
|
||||||
{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
|
|
||||||
@ -212,9 +212,6 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
|||||||
>
|
>
|
||||||
{dictionary['navigation'].vendor}
|
{dictionary['navigation'].vendor}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href={`/${locale}/draw`} icon={<i className='tabler-building' />} target='_blank'>
|
|
||||||
Random Draw
|
|
||||||
</MenuItem>
|
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
</Menu>
|
</Menu>
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user