diff --git a/src/app/[lang]/(blank-layout-pages)/draw/page.tsx b/src/app/[lang]/(blank-layout-pages)/draw/page.tsx index 4de2ee0..1217f13 100644 --- a/src/app/[lang]/(blank-layout-pages)/draw/page.tsx +++ b/src/app/[lang]/(blank-layout-pages)/draw/page.tsx @@ -1,34 +1,41 @@ 'use client'; import React, { useState, useEffect, useRef } from 'react'; -import { Users, RotateCw, Trophy, Trash2, RefreshCw, Database } from 'lucide-react'; +import { RotateCw, Trophy, Trash2, RefreshCw, Shuffle, Sparkles, Gift } from 'lucide-react'; +import { useVoucherRows } from '@/services/queries/vouchers'; +import { Voucher, VoucherRow } from '@/types/services/voucher'; +import { toast } from 'react-toastify'; -interface Order { - orderId: string; - customerName: string; - email: string; - amount: number; - status: string; +interface SpinRow { + id: string; + rowNumber: number; + vouchers: Voucher[]; + displayVouchers: Voucher[]; + isSpinning: boolean; + isShuffling: boolean; + winner: Voucher | null; + wheelRef: React.RefObject; + selectedWinner: Voucher | null; } -interface WinnerResult { - order: Order; +interface WinnerHistory { + rowNumber: number; + winner: Voucher; 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 [activeTab, setActiveTab] = useState<'acak' | 'hadiah' | 'winner'>('acak'); - const [shuffledDisplayOrder, setShuffledDisplayOrder] = useState([]); - const wheelRef = useRef(null); + const [spinRows, setSpinRows] = useState([]); + const [numberOfRows, setNumberOfRows] = useState(4); + const [winnersHistory, setWinnersHistory] = useState([]); + const [isRevealingWinners, setIsRevealingWinners] = useState(false); + const [revealedWinners, setRevealedWinners] = useState>(new Set()); const audioContextRef = useRef(null); - const tickIntervalRef = useRef(null); + const tickIntervalRefs = useRef>(new Map()); + const shuffleIntervalRefs = useRef>(new Map()); + + // Use React Query hook for fetching voucher data + const { data: voucherData, isLoading: isLoadingData, refetch: refetchVouchers, error: fetchError } = useVoucherRows({ rows: numberOfRows }); // Initialize Audio Context useEffect(() => { @@ -42,9 +49,8 @@ const RandomDrawApp: React.FC = () => { return () => { document.removeEventListener('click', initAudio); - if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current); - } + tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); + shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); }; }, []); @@ -70,10 +76,67 @@ const RandomDrawApp: React.FC = () => { oscillator.stop(ctx.currentTime + 0.05); }; + // Create drum roll sound + const playDrumRoll = () => { + if (!audioContextRef.current) return; + + const ctx = audioContextRef.current; + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + const noiseNode = ctx.createBufferSource(); + + // Create noise buffer for snare-like sound + const bufferSize = ctx.sampleRate * 0.05; + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + noiseNode.buffer = buffer; + + oscillator.connect(gainNode); + noiseNode.connect(gainNode); + gainNode.connect(ctx.destination); + + oscillator.frequency.setValueAtTime(200, ctx.currentTime); + gainNode.gain.setValueAtTime(0.05, ctx.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); + + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.1); + noiseNode.start(ctx.currentTime); + }; + + // Create winner sound + const playWinnerSound = () => { + if (!audioContextRef.current) return; + + const ctx = audioContextRef.current; + const notes = [523.25, 659.25, 783.99, 1046.5]; // C, E, G, C (higher octave) + + notes.forEach((freq, index) => { + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + + oscillator.frequency.setValueAtTime(freq, ctx.currentTime + index * 0.1); + + gainNode.gain.setValueAtTime(0, ctx.currentTime + index * 0.1); + gainNode.gain.linearRampToValueAtTime(0.2, ctx.currentTime + index * 0.1 + 0.05); + gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + index * 0.1 + 0.5); + + oscillator.start(ctx.currentTime + index * 0.1); + oscillator.stop(ctx.currentTime + index * 0.1 + 0.5); + }); + }; + // Start tick sound during spinning - const startTickSound = (duration: number) => { - if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current); + const startTickSound = (rowId: string, duration: number) => { + const existingInterval = tickIntervalRefs.current.get(rowId); + if (existingInterval) { + clearTimeout(existingInterval); } let tickInterval = 50; @@ -85,39 +148,23 @@ const RandomDrawApp: React.FC = () => { tickInterval += intervalIncrement; if (tickInterval < maxInterval) { - tickIntervalRef.current = setTimeout(tick, Math.min(tickInterval, maxInterval)); + const timeoutId = setTimeout(tick, Math.min(tickInterval, maxInterval)); + tickIntervalRefs.current.set(rowId, timeoutId); } }; tick(); setTimeout(() => { - if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current); - tickIntervalRef.current = null; + const interval = tickIntervalRefs.current.get(rowId); + if (interval) { + clearTimeout(interval); + tickIntervalRefs.current.delete(rowId); } }, 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)); - }, []); - + // Shuffle array const shuffleArray = (array: T[]): T[] => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { @@ -127,82 +174,268 @@ const RandomDrawApp: React.FC = () => { return shuffled; }; - const performSpin = (finalWinners: Order[], mainWinner: Order) => { - const wheelElement = wheelRef.current; + // Process voucher data when it changes + useEffect(() => { + if (voucherData && voucherData.rows) { + const newSpinRows: SpinRow[] = voucherData.rows.map(row => ({ + id: `row-${row.row_number}-${Date.now()}`, + rowNumber: row.row_number, + vouchers: row.vouchers, + displayVouchers: [...row.vouchers], + isSpinning: false, + isShuffling: false, + winner: null, + wheelRef: React.createRef(), + selectedWinner: null, + })); + setSpinRows(newSpinRows); + setRevealedWinners(new Set()); + } + }, [voucherData]); + + // Show error if fetch failed + useEffect(() => { + if (fetchError) { + toast.error('Failed to load voucher data. Please check your connection and try again.'); + } + }, [fetchError]); + + // Refetch when numberOfRows changes + useEffect(() => { + refetchVouchers(); + }, [numberOfRows]); + + // Shuffle all rows (Acak functionality) + const shuffleAllRows = () => { + setSpinRows(prev => prev.map(row => ({ + ...row, + isShuffling: true + }))); + + // Animate shuffling for each row + spinRows.forEach((row, index) => { + let shuffleCount = 0; + const maxShuffles = 20; + + const shuffleInterval = setInterval(() => { + shuffleCount++; + + setSpinRows(prev => prev.map(r => { + if (r.id === row.id) { + return { + ...r, + displayVouchers: shuffleArray(r.vouchers) + }; + } + return r; + })); + + playTickSound(); + + if (shuffleCount >= maxShuffles) { + clearInterval(shuffleInterval); + setTimeout(() => { + setSpinRows(prev => prev.map(r => { + if (r.id === row.id) { + return { + ...r, + isShuffling: false + }; + } + return r; + })); + }, index * 100); + } + }, 100); + + shuffleIntervalRefs.current.set(row.id, shuffleInterval as any); + }); + }; + + // Reveal winners dramatically + const revealWinnersDramatically = async () => { + setIsRevealingWinners(true); + setRevealedWinners(new Set()); + + // First, arrange winners to be in the winner zone for each row + const arrangedRows = spinRows.map(row => { + const winner = row.vouchers.find(v => v.is_winner); + if (!winner) return row; + + // Create a new display order with winner at position 3 or 4 (will be in winner zone) + const otherVouchers = row.vouchers.filter(v => v.voucher_code !== winner.voucher_code); + const shuffledOthers = shuffleArray(otherVouchers); + + // Place winner at index 3 (4th position) which will be in the winner zone + const newDisplayVouchers = [ + ...shuffledOthers.slice(0, 3), + winner, + ...shuffledOthers.slice(3) + ]; + + return { + ...row, + displayVouchers: newDisplayVouchers + }; + }); + + // Update display to show arranged vouchers + setSpinRows(arrangedRows); + + // Start drum roll effect + const drumRollInterval = setInterval(() => { + playDrumRoll(); + }, 100); + + // Create a scrolling/spinning effect for all wheels simultaneously + let scrollSpeed = 50; + let currentScroll = 0; + const maxScroll = 2000; + const deceleration = 0.95; + + const spinInterval = setInterval(() => { + currentScroll += scrollSpeed; + scrollSpeed *= deceleration; + + // Apply scrolling to all wheels + arrangedRows.forEach(row => { + const wheelElement = row.wheelRef.current; + if (wheelElement) { + wheelElement.style.transition = 'none'; + wheelElement.style.transform = `translateY(${-currentScroll}px)`; + } + }); + + // Stop when speed is very slow + if (scrollSpeed < 0.5 || currentScroll >= maxScroll) { + clearInterval(spinInterval); + clearInterval(drumRollInterval); + + // Final positioning - snap to winner position + arrangedRows.forEach(row => { + const wheelElement = row.wheelRef.current; + if (wheelElement) { + const CARD_HEIGHT = 80; + const WINNER_ZONE_TOP = 260; // Top position of winner zone + // Position 4th card (index 3) exactly at the winner zone + const finalPosition = -(3 * CARD_HEIGHT - WINNER_ZONE_TOP); + + wheelElement.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; + wheelElement.style.transform = `translateY(${finalPosition}px)`; + } + }); + } + }, 20); + + // Wait for scrolling to complete + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Play winner sound + playWinnerSound(); + + // Collect all winners + const winnersToReveal: { row: SpinRow, winner: Voucher }[] = []; + arrangedRows.forEach(row => { + const winner = row.vouchers.find(v => v.is_winner); + if (winner) { + winnersToReveal.push({ row, winner }); + } + }); + + // Update all winners at once with highlighting + const allWinnerNumbers = new Set(); + const historyEntries: WinnerHistory[] = []; + + winnersToReveal.forEach(({ row, winner }) => { + allWinnerNumbers.add(row.rowNumber); + historyEntries.push({ + rowNumber: row.rowNumber, + winner: winner, + timestamp: new Date() + }); + }); + + // Update state to highlight winners + setRevealedWinners(allWinnerNumbers); + + setSpinRows(prev => prev.map(r => { + const winnerInfo = winnersToReveal.find(w => w.row.id === r.id); + if (winnerInfo) { + return { + ...r, + winner: winnerInfo.winner, + selectedWinner: winnerInfo.winner + }; + } + return r; + })); + + setWinnersHistory(prev => [...prev, ...historyEntries]); + + setIsRevealingWinners(false); + }; + + const performSpin = (row: SpinRow) => { + const wheelElement = row.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); + const winner = row.vouchers.find(v => v.is_winner); + if (!winner) { + console.error('No winner found in vouchers'); + return; } + + const CARD_HEIGHT = 80; + const WINNER_ZONE_TOP = 260; // Top position of winner zone + + const winnerIndex = row.displayVouchers.findIndex(v => v.voucher_code === winner.voucher_code); - // 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 minSpins = 10; + const maxSpins = 20; const spins = minSpins + Math.random() * (maxSpins - minSpins); - const totalItems = currentWheelItems.length; + const totalItems = row.displayVouchers.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 finalPosition = -(baseScrollDistance + winnerScrollPosition) + WINNER_ZONE_TOP; - // 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); + startTickSound(row.id, duration); - // Complete spinning after animation setTimeout(() => { - // Stop sound - if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current); - tickIntervalRef.current = null; + const interval = tickIntervalRefs.current.get(row.id); + if (interval) { + clearTimeout(interval); + tickIntervalRefs.current.delete(row.id); } - // Set the ACTUAL pre-selected winner (not what's visually in wheel) - setSelectedWinner(mainWinner); + playWinnerSound(); - // Add PRE-SELECTED winners to results (guaranteed match) - const newWinners = finalWinners.map((order, index) => ({ - order, - timestamp: new Date(), - position: winners.length + index + 1 - })); + setSpinRows(prev => prev.map(r => + r.id === row.id + ? { ...r, isSpinning: false, winner: winner, selectedWinner: winner } + : r + )); - setWinners(prev => [...prev, ...newWinners]); - setIsSpinning(false); + setWinnersHistory(prev => [...prev, { + rowNumber: row.rowNumber, + winner: winner, + timestamp: new Date() + }]); - 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)'; @@ -213,414 +446,346 @@ const RandomDrawApp: React.FC = () => { }, 100); }; - const spinWheel = async () => { - // Filter orders yang belum menang - const availableOrders = orders.filter(order => - !winners.some(winner => winner.order.orderId === order.orderId) - ); + const spinWheel = (rowId: string) => { + const row = spinRows.find(r => r.id === rowId); + if (!row || row.isSpinning || row.winner) return; - if (availableOrders.length === 0 || isSpinning) return; + setSpinRows(prev => prev.map(r => + r.id === rowId ? { ...r, isSpinning: true, selectedWinner: null } : r + )); - setIsSpinning(true); - setSelectedWinner(null); - - // Reset wheel position before every spin - const wheelElement = wheelRef.current; + const wheelElement = row.wheelRef.current; if (wheelElement) { wheelElement.style.transition = 'none'; wheelElement.style.transform = 'translateY(0px)'; - wheelElement.offsetHeight; // Force reflow + wheelElement.offsetHeight; } - // 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); - } + performSpin(row); }; const clearResults = () => { - // Stop any ongoing sound effects - if (tickIntervalRef.current) { - clearTimeout(tickIntervalRef.current); - tickIntervalRef.current = null; - } + tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); + shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); + tickIntervalRefs.current.clear(); + shuffleIntervalRefs.current.clear(); - setWinners([]); - setSelectedWinner(null); - setIsSpinning(false); // Force reset spinning state + setWinnersHistory([]); + setRevealedWinners(new Set()); + setSpinRows(prev => prev.map(row => ({ + ...row, + isSpinning: false, + isShuffling: false, + winner: null, + selectedWinner: null, + displayVouchers: [...row.vouchers] + }))); - // Reset wheel position when clearing - const wheelElement = wheelRef.current; - if (wheelElement) { - wheelElement.style.transition = 'none'; - wheelElement.style.transform = 'translateY(0px)'; + spinRows.forEach(row => { + const wheelElement = row.wheelRef.current; + if (wheelElement) { + wheelElement.style.transition = 'none'; + wheelElement.style.transform = 'translateY(0px)'; + } + }); + }; + + const refreshData = async () => { + tickIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); + shuffleIntervalRefs.current.forEach(timeout => clearTimeout(timeout)); + tickIntervalRefs.current.clear(); + shuffleIntervalRefs.current.clear(); + + try { + await refetchVouchers(); + toast.success('Data refreshed successfully'); + } catch (error) { + toast.error('Failed to refresh data'); } }; - 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 formatPhoneNumber = (phone: string) => { + return phone.substring(0, 5) + '****' + phone.substring(phone.length - 2); }; - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 - }).format(amount); - }; + return ( +
+ + +
+ {/* Header */} +
+

+ Voucher Lucky Draw +

+

Select random winners from voucher database

+
- // 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 ( -
- {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 && ( - - )} -
-
- ); - })} -
- ))} -
- ); - }; - - const renderTabContent = () => { - switch (activeTab) { - case 'acak': - return ( -
- {/* Controls */} +
+ {/* Controls Section */} +
+ {/* Configuration Card */}
-
-

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

+

+ Configuration +

+ +
+
+ + setNumberOfRows(parseInt(e.target.value) || 1)} + className="w-full bg-white border border-gray-300 rounded-lg p-2 text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" + disabled={isLoadingData} + /> +
+
- -
- - 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} - /> -
- - + + {spinRows.length > 0 && ( +
+
+
Rows: {spinRows.length}
+
Total Vouchers: {spinRows.reduce((acc, row) => acc + row.vouchers.length, 0)}
+
+
+ )}
- {/* Grid Table */} + {/* Actions Card */}
-
-

Eligible Orders

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

Prize Management

-

Prize configuration will be available here.

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

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

+

+ Actions +

+ +
+ + + + + {winnersHistory.length > 0 && ( -
- - {/* Simple Winner List */} -
- {winners.map((winner) => ( + )} +
+
+ + {/* Winners Summary Card */} +
+

+ + Winners +

+ + {winnersHistory.length > 0 ? ( +
+ {winnersHistory.map((history) => (
-
-
- #{winner.position} -
-
-
{winner.order.customerName}
-
{winner.order.orderId} • {winner.order.email}
-
+
+ Row {history.rowNumber}: {history.winner.name}
- -
-
{formatCurrency(winner.order.amount)}
-
Won at: {winner.timestamp.toLocaleTimeString()}
+
+ {history.winner.voucher_code}
))}
-
- ) : ( -
-
- -

No Winners Yet

-

Start spinning to see the winners here!

+ ) : ( +
+ +

No winners yet

-
- )} -
- ); - - 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

- - {/* 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) => - currentWheelItems.map((order, orderIndex) => { - 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)} -
- {(isCurrentWinner || isSelectedWinner) && ( - - )} -
-
- ); - }) - )} -
- - {/* Gradient Overlays */} -
-
+ )}
+ + {/* Spin Rows - Display in grid */} +
+ {spinRows.map((row) => ( +
+
+
+

+ Row {row.rowNumber} + ({row.vouchers.length}) +

+ + +
+ + {row.winner && ( +
+
+ + + {row.winner.name} + +
+
+ {row.winner.voucher_code} +
+
+ )} +
+ + {/* Wheel Container */} +
+ {/* Winner Selection Zone - Exactly 80px matching card height */} +
+
+ WINNER +
+
+ + {/* Scrolling Cards */} +
+ {Array.from({ length: 50 }, (_, repeatIndex) => + row.displayVouchers.map((voucher, voucherIndex) => { + const isSelectedWinner = row.selectedWinner?.voucher_code === voucher.voucher_code; + + return ( +
+
+
+ {voucher.voucher_code} +
+
+ {voucher.name} +
+
+ +
+
+ {formatPhoneNumber(voucher.phone_number)} +
+ {isSelectedWinner && ( + + )} +
+
+ ); + }) + )} +
+ + {/* Gradient Overlays */} +
+
+
+
+ ))} +
); }; -export default RandomDrawApp; +export default RandomDrawApp; \ No newline at end of file diff --git a/src/services/queries/vouchers.ts b/src/services/queries/vouchers.ts new file mode 100644 index 0000000..1d4cdf4 --- /dev/null +++ b/src/services/queries/vouchers.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query' +import { VoucherRowsResponse } from '../../types/services/voucher' +import { api } from '../api' + +export interface VouchersQueryParams { + rows?: number +} + +export function useVoucherRows(params: VouchersQueryParams = {}) { + const { rows = 4 } = params + + return useQuery({ + queryKey: ['voucher-rows', { rows }], + queryFn: async () => { + const res = await api.get(`/vouchers/rows`, { + params: { rows } + }) + return res.data.data + }, + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false + }) +} + +// Manual fetch function for cases where you need to fetch without using the hook +export async function fetchVoucherRows(rows: number = 4): Promise { + const res = await api.get(`/vouchers/rows`, { + params: { rows } + }) + return res.data.data +} \ No newline at end of file diff --git a/src/types/services/voucher.ts b/src/types/services/voucher.ts new file mode 100644 index 0000000..03eb4b9 --- /dev/null +++ b/src/types/services/voucher.ts @@ -0,0 +1,23 @@ +export interface Voucher { + voucher_code: string + name: string + phone_number: string + is_winner: boolean +} + +export interface VoucherRow { + row_number: number + vouchers: Voucher[] +} + +export interface VoucherRowsResponse { + rows: VoucherRow[] + total_rows: number + total_vouchers: number +} + +export interface VoucherApiResponse { + success: boolean + data: VoucherRowsResponse + errors: any +} \ No newline at end of file