Spaces:
Paused
Paused
| import React, { useState, useEffect } from 'react'; | |
| import { X, Gift } from 'lucide-react'; | |
| import { Language, AdminConfig, UserAccount } from '../types'; | |
| import { TRANSLATIONS } from '../constants/translations'; | |
| // @ts-ignore | |
| import confetti from 'canvas-confetti'; | |
| interface RedPacketModalProps { | |
| isVisible: boolean; | |
| onClose: () => void; | |
| language: Language; | |
| adminConfig: AdminConfig; | |
| user?: UserAccount; | |
| onUpdateUser: (balance: number) => void; | |
| onOpenShare: () => void; | |
| } | |
| export const RedPacketModal: React.FC<RedPacketModalProps> = ({ | |
| isVisible, onClose, language, adminConfig, user, onUpdateUser, onOpenShare | |
| }) => { | |
| const [stage, setStage] = useState<'closed' | 'opened'>('closed'); | |
| const [amount, setAmount] = useState(0); | |
| const t = TRANSLATIONS[language]; | |
| // Determine target amount | |
| const max = adminConfig.redPacketMax || 2000; | |
| // If user exists, show their balance. If no user (guest), show default almost-max (e.g. max - 20) | |
| const userBalance = user?.redPacketBalance ?? (max - 20); | |
| useEffect(() => { | |
| if (isVisible) { | |
| setStage('closed'); | |
| // Animate amount up to userBalance | |
| let current = 0; | |
| // Animation speed | |
| const step = userBalance / 40; | |
| const interval = setInterval(() => { | |
| current += step; | |
| if (current >= userBalance) { | |
| current = userBalance; | |
| clearInterval(interval); | |
| } | |
| setAmount(Math.floor(current)); | |
| }, 30); | |
| return () => clearInterval(interval); | |
| } | |
| }, [isVisible, userBalance]); | |
| if (!isVisible) return null; | |
| const handleOpen = () => { | |
| setStage('opened'); | |
| confetti({ | |
| particleCount: 150, | |
| spread: 80, | |
| origin: { y: 0.6 }, | |
| colors: ['#fbbf24', '#ef4444', '#ffffff'] | |
| }); | |
| // Ensure user gets this balance if they haven't already | |
| if (user && (user.redPacketBalance === undefined || user.redPacketBalance === 0)) { | |
| onUpdateUser(userBalance); | |
| } | |
| }; | |
| const handleWithdraw = () => { | |
| onOpenShare(); | |
| onClose(); | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-[120] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in"> | |
| <div className="relative w-full max-w-sm"> | |
| <button onClick={onClose} className="absolute -top-10 right-0 p-2 bg-white/20 rounded-full text-white hover:bg-white/40"> | |
| <X className="w-5 h-5" /> | |
| </button> | |
| {stage === 'closed' ? ( | |
| // Stage 1: The Big Red Envelope | |
| <div className="bg-gradient-to-b from-red-500 to-red-600 rounded-3xl shadow-2xl overflow-hidden text-center p-8 relative animate-bounce-slow"> | |
| <div className="absolute top-0 left-1/2 -translate-x-1/2 w-32 h-32 bg-red-700 rounded-b-full opacity-50"></div> | |
| <div className="relative z-10 pt-10"> | |
| <p className="text-yellow-200 text-lg font-bold mb-2">{t.pddRedPacketTitle}</p> | |
| <h3 className="text-3xl font-bold text-white mb-8">{t.pddRedPacketDesc}</h3> | |
| <button | |
| onClick={handleOpen} | |
| className="w-24 h-24 rounded-full bg-yellow-400 border-4 border-yellow-200 text-red-600 font-bold text-xl shadow-lg hover:scale-110 transition-transform flex items-center justify-center mx-auto" | |
| > | |
| <span className="animate-pulse">{t.pddRedPacketCta}</span> | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| // Stage 2: The "Almost There" Trap | |
| <div className="bg-white rounded-3xl shadow-2xl overflow-hidden relative"> | |
| <div className="bg-red-500 p-6 text-center pb-12"> | |
| <p className="text-white/80 text-sm font-bold uppercase tracking-wider">Account Balance</p> | |
| <h2 className="text-5xl font-bold text-white mt-2">¥{amount}</h2> | |
| </div> | |
| <div className="px-6 -mt-8 relative z-10"> | |
| <div className="bg-white rounded-xl shadow-lg p-6 border border-gray-100 text-center"> | |
| <p className="text-gray-800 font-bold text-lg">{t.pddWithdrawTitle}</p> | |
| <div className="w-full h-3 bg-gray-100 rounded-full mt-3 overflow-hidden"> | |
| <div className="h-full bg-red-500 animate-pulse" style={{ width: `${(amount / max) * 100}%` }}></div> | |
| </div> | |
| <p className="text-xs text-red-500 mt-2 font-bold">{t.pddWithdrawDesc}</p> | |
| <button | |
| onClick={handleWithdraw} | |
| className="w-full py-3 bg-red-500 text-white rounded-full font-bold mt-4 shadow-lg shadow-red-200 hover:bg-red-600 flex items-center justify-center gap-2" | |
| > | |
| <Gift className="w-4 h-4" /> Share to Withdraw | |
| </button> | |
| </div> | |
| </div> | |
| <div className="p-6 bg-gray-50"> | |
| <div className="space-y-3"> | |
| {['User 123 withdrew ¥2000', 'Amy unlocked VIP', 'Mike got ¥500'].map((txt, i) => ( | |
| <div key={i} className="flex items-center gap-2 text-xs text-gray-500"> | |
| <div className="w-6 h-6 bg-gray-200 rounded-full"></div> | |
| <span>{txt}</span> | |
| <span className="ml-auto text-gray-400">1m ago</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; |