import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Modal, Animated, StyleSheet, ScrollView, FlatList, TextInput, Platform, } from 'react-native'; import { useDubsTheme } from '../theme'; import { useDubs } from '../../provider'; import { useJackpot } from '../../hooks/useJackpot'; import { useEnterJackpot } from '../../hooks/useEnterJackpot'; import type { EnterJackpotMutationResult } from '../../hooks/useEnterJackpot'; import type { JackpotEntry } from '../../types'; export interface JackpotSheetProps { visible: boolean; onDismiss: () => void; onSuccess?: (result: EnterJackpotMutationResult) => void; onError?: (error: Error) => void; minEntry?: number; maxEntry?: number; } const STATUS_LABELS: Record = { building: 'Building transaction...', signing: 'Approve in wallet...', confirming: 'Confirming entry...', success: "You're in! 🏆", }; const QUICK_AMOUNTS = [0.1, 0.5, 1]; function formatSOL(lamports: string | number): string { const val = typeof lamports === 'string' ? parseInt(lamports, 10) : lamports; if (isNaN(val) || val === 0) return '0'; const sol = val / 1_000_000_000; if (sol >= 100) return sol.toFixed(0); if (sol >= 1) return sol.toFixed(2); if (sol >= 0.01) return sol.toFixed(3); return sol.toFixed(4); } function truncateWallet(addr: string): string { if (!addr || addr.length < 8) return addr || ''; return `${addr.slice(0, 4)}...${addr.slice(-4)}`; } export function JackpotSheet({ visible, onDismiss, onSuccess, onError, minEntry = 0.01, maxEntry = 10, }: JackpotSheetProps) { const t = useDubsTheme(); const { wallet, client } = useDubs(); const { round, lastWinner, refetch } = useJackpot(); const mutation = useEnterJackpot(); const [betAmount, setBetAmount] = useState('0.1'); const [entries, setEntries] = useState([]); const overlayOpacity = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.timing(overlayOpacity, { toValue: visible ? 1 : 0, duration: 250, useNativeDriver: true, }).start(); }, [visible, overlayOpacity]); // Fetch entries when sheet opens useEffect(() => { if (visible) { mutation.reset(); setBetAmount('0.1'); refetch(); client.getJackpotEntries().then(res => setEntries(res.entries)).catch(() => {}); } }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps // Auto-dismiss on success useEffect(() => { if (mutation.status === 'success' && mutation.data) { onSuccess?.(mutation.data); refetch(); client.getJackpotEntries().then(res => setEntries(res.entries)).catch(() => {}); const timer = setTimeout(() => onDismiss(), 2500); return () => clearTimeout(timer); } }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (mutation.status === 'error' && mutation.error) { onError?.(mutation.error); } }, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps const potSol = round ? Number(BigInt(round.totalPotLamports || '0')) / 1_000_000_000 : 0; const totalWeight = round ? Number(BigInt(round.totalWeight || '0')) : 0; const betSol = parseFloat(betAmount) || 0; const entryLamports = Math.round(betSol * 1_000_000_000); // Live odds preview const userOdds = totalWeight > 0 ? ((entryLamports / (totalWeight + entryLamports)) * 100).toFixed(1) : entries.length === 0 ? '100.0' : '0.0'; const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error'; const canEnter = !isMutating && mutation.status !== 'success' && round?.status === 'Open' && betSol >= minEntry; const handleQuickAdd = useCallback((amount: number) => { setBetAmount(prev => { const current = parseFloat(prev) || 0; const next = Math.min(current + amount, maxEntry); return next.toFixed(2); }); }, [maxEntry]); const handleEnter = useCallback(async () => { if (!wallet.publicKey || entryLamports < 10000) return; try { await mutation.execute(entryLamports); } catch { // Error captured in mutation state } }, [wallet.publicKey, mutation.execute, entryLamports]); // eslint-disable-line react-hooks/exhaustive-deps const statusLabel = STATUS_LABELS[mutation.status] || ''; const renderPlayerCard = ({ item, index }: { item: JackpotEntry; index: number }) => ( {/* Avatar */} {item.player.slice(0, 2).toUpperCase()} {/* Username */} @{truncateWallet(item.player)} {/* Wager */} {item.weightSol.toFixed(2)} SOL {/* Odds */} {item.oddsPercent}% ); return ( {/* Drag handle */} {/* Header */} Jackpot {'\u2715'} {/* Hero pot card */} JACKPOT {potSol.toFixed(4)} SOL 🤑 {/* Accent bar */} {/* Info cards row */} YOUR WAGER {betSol.toFixed(2)} YOUR CHANCE {userOdds}% PLAYERS {entries.length} {/* Player carousel */} {entries.length > 0 && ( Players in Round {entries.length} active `${item.player}-${i}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.playersListContent} ItemSeparatorComponent={() => } /> )} {/* Empty state */} {entries.length === 0 && ( 🤑 No players yet Be the first to join! )} {/* Bet input section */} Bet Amount {/* Input row */} SOL {/* Quick bet buttons */} {QUICK_AMOUNTS.map(amount => ( handleQuickAdd(amount)} activeOpacity={0.7} > +{amount} SOL ))} {/* Error */} {mutation.error && ( {mutation.error.message} )} {/* Place Bet CTA */} {isMutating ? ( {statusLabel} ) : mutation.status === 'success' ? ( You're in! 🏆 ) : ( Place Bet — {betSol.toFixed(2)} SOL )} {/* Last winner */} {lastWinner && ( Last Winner {truncateWallet(lastWinner.winner)} won {formatSOL(lastWinner.winAmount)} SOL )} ); } const styles = StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.65)', }, overlayTap: { flex: 1 }, sheetPositioner: { flex: 1, justifyContent: 'flex-end', }, sheet: { backgroundColor: '#08080e', borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: '92%', borderWidth: 1, borderColor: '#1e1e2a', borderBottomWidth: 0, }, handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 4 }, handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: '#2a2a3a' }, scrollView: { flexGrow: 0 }, scrollContent: { paddingHorizontal: 16, paddingBottom: Platform.OS === 'ios' ? 40 : 24, }, // Header header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, }, headerTitle: { fontSize: 20, fontWeight: '700', color: '#FFFFFF' }, closeBtn: { padding: 4 }, closeBtnText: { fontSize: 20, color: '#6b6b6b' }, // Hero pot card heroCard: { borderRadius: 16, borderWidth: 1, borderColor: 'rgba(34, 197, 94, 0.2)', padding: 16, overflow: 'hidden', position: 'relative', }, heroGradient: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(34, 197, 94, 0.04)', }, heroLabel: { fontSize: 10, fontWeight: '600', color: '#6b6b6b', letterSpacing: 3, marginBottom: 4, }, heroPotRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, heroPotValue: { fontSize: 32, fontWeight: '900', color: '#4ade80', letterSpacing: -1, }, heroEmoji: { fontSize: 36, opacity: 0.2, }, heroAccentBar: { position: 'absolute', bottom: 0, left: 0, width: '60%', height: 2, backgroundColor: '#22c55e', }, // Info row infoRow: { flexDirection: 'row', gap: 8, marginTop: 12, }, infoCard: { flex: 1, borderRadius: 12, borderWidth: 1, borderColor: '#1e1e2a', backgroundColor: '#0c0c14', padding: 12, }, infoCardLabel: { fontSize: 9, fontWeight: '600', color: '#6b6b6b', letterSpacing: 2, marginBottom: 4, }, infoCardValue: { fontSize: 18, fontWeight: '700', color: '#FFFFFF', }, // Players section playersSection: { marginTop: 16, borderRadius: 16, borderWidth: 1, borderColor: '#1e1e2a', backgroundColor: 'rgba(139, 92, 246, 0.03)', padding: 12, overflow: 'hidden', }, playersSectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, }, playersSectionTitle: { fontSize: 13, fontWeight: '600', color: '#a0a0a0', }, playersBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: 'rgba(34, 197, 94, 0.15)', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 100, }, playersBadgeDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#22c55e', }, playersBadgeText: { fontSize: 11, fontWeight: '700', color: '#22c55e', }, playersListContent: { paddingRight: 4, }, // Player cards (carousel items) playerCard: { width: 110, borderRadius: 12, borderWidth: 1.5, borderColor: 'rgba(139, 92, 246, 0.4)', backgroundColor: 'rgba(139, 92, 246, 0.08)', padding: 12, alignItems: 'center', gap: 6, }, playerAvatar: { width: 48, height: 48, borderRadius: 24, borderWidth: 2, borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.15)', alignItems: 'center', justifyContent: 'center', }, playerAvatarText: { fontSize: 16, fontWeight: '700', color: '#a78bfa', }, playerUsername: { fontSize: 11, fontWeight: '500', color: '#a0a0a0', textAlign: 'center', }, playerWagerRow: { flexDirection: 'row', alignItems: 'center', gap: 4, }, playerWagerAmount: { fontSize: 13, fontWeight: '600', color: '#a78bfa', }, playerOdds: { fontSize: 11, fontWeight: '700', color: '#22c55e', }, // Empty state emptyState: { marginTop: 16, alignItems: 'center', paddingVertical: 24, borderRadius: 16, borderWidth: 1, borderColor: '#1e1e2a', backgroundColor: '#0c0c14', }, emptyEmoji: { fontSize: 40, marginBottom: 8 }, emptyTitle: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' }, emptySubtitle: { fontSize: 13, color: '#6b6b6b', marginTop: 4 }, // Bet section betSection: { marginTop: 16, }, betLabel: { fontSize: 13, fontWeight: '500', color: '#6b6b6b', marginBottom: 8, }, betInputRow: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: '#08080e', borderWidth: 1, borderColor: '#2a2a3a', borderRadius: 12, paddingHorizontal: 14, paddingVertical: Platform.OS === 'ios' ? 14 : 10, }, solIcon: { width: 32, height: 32, borderRadius: 16, backgroundColor: 'rgba(139, 92, 246, 0.15)', alignItems: 'center', justifyContent: 'center', }, solIconText: { fontSize: 18, color: '#a78bfa', fontWeight: '700', }, betInput: { flex: 1, fontSize: 24, fontWeight: '700', color: '#FFFFFF', padding: 0, }, betInputSuffix: { fontSize: 14, fontWeight: '500', color: '#6b6b6b', }, quickBetRow: { flexDirection: 'row', gap: 8, marginTop: 10, }, quickBetBtn: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1, borderColor: '#2a2a3a', backgroundColor: '#08080e', alignItems: 'center', }, quickBetText: { fontSize: 13, fontWeight: '600', color: '#FFFFFF', }, // Error errorBox: { marginTop: 12, borderRadius: 12, borderWidth: 1, borderColor: '#3A1515', backgroundColor: '#1A0A0A', padding: 12, }, errorText: { fontSize: 13, fontWeight: '500', color: '#F87171' }, // Place Bet CTA placeBetBtn: { marginTop: 16, height: 56, borderRadius: 14, justifyContent: 'center', alignItems: 'center', backgroundColor: '#22c55e', shadowColor: '#22c55e', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.25, shadowRadius: 12, elevation: 6, }, placeBetBtnDisabled: { backgroundColor: '#1e1e2a', shadowOpacity: 0, elevation: 0, }, placeBetBtnSuccess: { backgroundColor: '#16a34a', }, placeBetText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' }, placeBetLoading: { flexDirection: 'row', alignItems: 'center', gap: 10 }, // Last winner lastWinnerSection: { marginTop: 14, paddingTop: 14, borderTopWidth: 1, borderTopColor: '#1e1e2a', }, lastWinnerLabel: { fontSize: 11, fontWeight: '600', color: '#6b6b6b', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 4, }, lastWinnerRow: { flexDirection: 'row', alignItems: 'center', gap: 6, }, lastWinnerWallet: { fontSize: 13, fontWeight: '500', color: '#a0a0a0', }, lastWinnerAmount: { fontSize: 13, fontWeight: '600', color: '#22c55e', }, });