From d85f081d3a5e08f2b9c346ccd4c711ffce019ba3 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 13 Sep 2025 15:06:46 +0700 Subject: [PATCH] Update draw --- .../[lang]/(blank-layout-pages)/draw/page.tsx | 845 +++++++++--------- 1 file changed, 435 insertions(+), 410 deletions(-) diff --git a/src/app/[lang]/(blank-layout-pages)/draw/page.tsx b/src/app/[lang]/(blank-layout-pages)/draw/page.tsx index dc71484..4de2ee0 100644 --- a/src/app/[lang]/(blank-layout-pages)/draw/page.tsx +++ b/src/app/[lang]/(blank-layout-pages)/draw/page.tsx @@ -1,372 +1,376 @@ -'use client' +'use client'; -import React, { useState, useEffect, useRef } from 'react' -import { Users, RotateCw, Trophy, Trash2, RefreshCw, Database } from 'lucide-react' +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 + orderId: string; + customerName: string; + email: string; + amount: number; + status: string; } interface WinnerResult { - order: Order - timestamp: Date - position: number + order: Order; + timestamp: Date; + position: number; } const RandomDrawApp: React.FC = () => { - const [orders, setOrders] = useState([]) - const [isSpinning, setIsSpinning] = useState(false) - const [winners, setWinners] = useState([]) - const [numberOfWinners, setNumberOfWinners] = useState(1) - const [currentWheelItems, setCurrentWheelItems] = useState([]) - const [selectedWinner, setSelectedWinner] = useState(null) - const wheelRef = useRef(null) - const audioContextRef = useRef(null) - const tickIntervalRef = useRef(null) + const [orders, setOrders] = useState([]); + const [isSpinning, setIsSpinning] = useState(false); + const [winners, setWinners] = useState([]); + const [numberOfWinners, setNumberOfWinners] = useState(1); + const [currentWheelItems, setCurrentWheelItems] = useState([]); + const [selectedWinner, setSelectedWinner] = useState(null); + const [activeTab, setActiveTab] = useState<'acak' | 'hadiah' | 'winner'>('acak'); + const [shuffledDisplayOrder, setShuffledDisplayOrder] = useState([]); + const wheelRef = useRef(null); + const audioContextRef = useRef(null); + const tickIntervalRef = useRef(null); // Initialize Audio Context useEffect(() => { const initAudio = () => { 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 () => { - document.removeEventListener('click', initAudio) + document.removeEventListener('click', initAudio); if (tickIntervalRef.current) { - clearTimeout(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) - } + 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) + clearTimeout(tickIntervalRef.current); } - - let tickInterval = 50 - const maxInterval = 200 - const intervalIncrement = (maxInterval - tickInterval) / (duration / 100) - + + let tickInterval = 50; + const maxInterval = 200; + const intervalIncrement = (maxInterval - tickInterval) / (duration / 100); + const tick = () => { - playTickSound() - tickInterval += intervalIncrement - + playTickSound(); + tickInterval += intervalIncrement; + if (tickInterval < maxInterval) { - tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval)) + tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval)); } - } - - tick() - + }; + + tick(); + setTimeout(() => { if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current) - tickIntervalRef.current = null + clearTimeout(tickIntervalRef.current); + tickIntervalRef.current = null; } - }, duration + 100) - } + }, duration + 100); + }; + + // Generate random order ID + const generateRandomOrderId = () => { + return Math.floor(100000000 + Math.random() * 900000000).toString(); + }; // 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 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 = (array: T[]): T[] => { - const shuffled = [...array] + 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]] + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } - return shuffled - } + return shuffled; + }; const performSpin = (finalWinners: Order[], mainWinner: Order) => { - const wheelElement = wheelRef.current - if (!wheelElement) return - - const CARD_HEIGHT = 100 - const WINNER_ZONE_CENTER = 250 + 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) - + 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) + winnerIndex = Math.floor(Math.random() * currentWheelItems.length); } - + // Reset wheel position - wheelElement.style.transition = 'none' - wheelElement.style.transform = 'translateY(0px)' - wheelElement.offsetHeight - + 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 - + 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 - + 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) - + 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)` - + 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) - + startTickSound(duration); + // Complete spinning after animation setTimeout(() => { // Stop sound if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current) - tickIntervalRef.current = null + clearTimeout(tickIntervalRef.current); + tickIntervalRef.current = null; } - + // Set the ACTUAL pre-selected winner (not what's visually in wheel) - setSelectedWinner(mainWinner) - + 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)') - + })); + + 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) - } + 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) - + 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 + const wheelElement = wheelRef.current; if (wheelElement) { - wheelElement.style.transition = 'none' - wheelElement.style.transform = 'translateY(0px)' - wheelElement.offsetHeight // Force reflow + 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') - + 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) + performSpin(finalWinners, mainWinner); } - } + }; const clearResults = () => { // Stop any ongoing sound effects if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current) - tickIntervalRef.current = null + clearTimeout(tickIntervalRef.current); + tickIntervalRef.current = null; } - - setWinners([]) - setSelectedWinner(null) - setIsSpinning(false) // Force reset spinning state - + + setWinners([]); + setSelectedWinner(null); + setIsSpinning(false); // Force reset spinning state + // Reset wheel position when clearing - const wheelElement = wheelRef.current + const wheelElement = wheelRef.current; if (wheelElement) { - wheelElement.style.transition = 'none' - wheelElement.style.transform = 'translateY(0px)' + 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 + clearTimeout(tickIntervalRef.current); + tickIntervalRef.current = null; } - - const shuffled = shuffleArray(orders) - setCurrentWheelItems(shuffled) - setSelectedWinner(null) - setIsSpinning(false) // Force reset spinning state - + + 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 + const wheelElement = wheelRef.current; if (wheelElement) { - wheelElement.style.transition = 'none' - wheelElement.style.transform = 'translateY(0px)' + 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) - } + }).format(amount); + }; // Get available orders for spinning - const availableOrdersCount = orders.filter( - order => !winners.some(winner => winner.order.orderId === order.orderId) - ).length + const availableOrdersCount = orders.filter(order => + !winners.some(winner => winner.order.orderId === order.orderId) + ).length; - return ( -
-
- {/* Header */} -
-

Random Order Draw

-

Select random winners from order database

-
+ const GridTable = ({ data, columns = 4 }: { data: Order[], columns?: number }) => { + const rows = Math.ceil(data.length / columns); + + return ( +
+ {Array.from({ length: columns }, (_, colIndex) => ( +
+ {Array.from({ length: rows }, (_, rowIndex) => { + const itemIndex = rowIndex * columns + colIndex; + const item = shuffledDisplayOrder[itemIndex]; + + if (!item) return
; + + const isWinner = winners.some(winner => winner.order.orderId === item.orderId); + + return ( +
+
+ {item.orderId} + {isWinner && ( + + )} +
+
+ ); + })} +
+ ))} +
+ ); + }; - {/* Main Layout */} -
- {/* Left Panel - Table & Controls */} -
+ const renderTabContent = () => { + switch (activeTab) { + case 'acak': + return ( +
{/* Controls */} -
-
-

- +
+
+

+ Order Database ({orders.length} total, {availableOrdersCount} available)

-
- +
+ 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' + 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} />
@@ -374,76 +378,146 @@ const RandomDrawApp: React.FC = () => {
- {/* Orders Table */} -
-
-

Eligible Orders

-
-
- - - - - - - - - - - {orders.map(order => { - const isWinner = winners.some(winner => winner.order.orderId === order.orderId) - return ( - - - - - - - ) - })} - -
Order IDCustomerAmountStatus
{order.orderId} -
{order.customerName}
-
{order.email}
-
{formatCurrency(order.amount)} - - {order.status} - - {isWinner && } -
+ {/* Grid Table */} +
+
+

Eligible Orders

+
+ ); + + case 'hadiah': + return ( +
+

Prize Management

+

Prize configuration will be available here.

+
+ ); + + case 'winner': + return ( +
+ {winners.length > 0 ? ( +
+
+

+ + Draw Results ({winners.length} Winners) +

+ +
+ + {/* Simple Winner List */} +
+ {winners.map((winner) => ( +
+
+
+ #{winner.position} +
+
+
{winner.order.customerName}
+
{winner.order.orderId} • {winner.order.email}
+
+
+ +
+
{formatCurrency(winner.order.amount)}
+
Won at: {winner.timestamp.toLocaleTimeString()}
+
+
+ ))} +
+
+ ) : ( +
+
+ +

No Winners Yet

+

Start spinning to see the winners here!

+
+
+ )} +
+ ); + + default: + return null; + } + }; + + return ( +
+
+ {/* Header */} +
+

Random Order Draw

+

Select random winners from order database

+
+ + {/* Tabs */} +
+
+ {[ + { id: 'acak', label: 'acak' }, + { id: 'hadiah', label: 'hadiah' }, + { id: 'winner', label: 'winner' } + ].map((tab) => ( + + ))} +
+
+ + {/* Main Layout */} +
+ + {/* Left Panel - Tab Content */} +
+ {renderTabContent()} +
{/* Right Panel - Spin Wheel */} -
-

Spin Wheel

- +
+

Spin Wheel

+ {/* Wheel Container */} -
{/* Winner Selection Zone */} -
{ }} > {/* Left Arrow */} -
- + {/* Winner Badge */} -
+
🎯 WINNER 🎯
- + {/* Right Arrow */} -
{/* Scrolling Cards */} -
+
{/* Generate enough cards for smooth scrolling */} - {Array.from({ length: 40 }, (_, repeatIndex) => + {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 - + const isCurrentWinner = winners.some(winner => winner.order.orderId === order.orderId); + const isSelectedWinner = selectedWinner?.orderId === order.orderId; + return (
{/* Order Info */} -
-
+
+
{order.orderId}
-
+
{order.customerName}
- + {/* Amount */} -
-
{formatCurrency(order.amount)}
+
+
+ {formatCurrency(order.amount)} +
{(isCurrentWinner || isSelectedWinner) && ( - + )}
- ) + ); }) )}
- + {/* Gradient Overlays */} -
-
+
+
- - {/* Results Section */} - {winners.length > 0 && ( -
-
-

- - Draw Results ({winners.length} Winners) -

- -
- - {/* Simple Winner List */} -
- {winners.map(winner => ( -
-
-
- #{winner.position} -
-
-
{winner.order.customerName}
-
- {winner.order.orderId} • {winner.order.email} -
-
-
- -
-
{formatCurrency(winner.order.amount)}
-
Won at: {winner.timestamp.toLocaleTimeString()}
-
-
- ))} -
-
- )}
- ) -} + ); +}; -export default RandomDrawApp +export default RandomDrawApp; -- 2.47.2