Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { | |
| MessageSquare, | |
| Users, | |
| Send, | |
| QrCode, | |
| CheckCircle2, | |
| Play, | |
| Terminal, | |
| LogOut, | |
| AlertCircle, | |
| Unlock, | |
| ShieldCheck, | |
| RefreshCw | |
| } from 'lucide-react'; | |
| type Status = 'DISCONNECTED' | 'INITIALIZING' | 'QR_CODE' | 'CONNECTED' | 'ERROR'; | |
| function App() { | |
| const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false); | |
| const [passkey, setPasskey] = useState(''); | |
| const [loginError, setLoginError] = useState(''); | |
| const [isLoggingIn, setIsLoggingIn] = useState(false); | |
| const [status, setStatus] = useState<Status>('DISCONNECTED'); | |
| const [qrCode, setQrCode] = useState(''); | |
| const [logs, setLogs] = useState<string[]>([]); | |
| const [phone, setPhone] = useState(''); | |
| const [message, setMessage] = useState(''); | |
| const [isSending, setIsSending] = useState(false); | |
| const [activeTab, setActiveTab] = useState<'direct' | 'group'>('direct'); | |
| const [groups, setGroups] = useState<any[]>([]); | |
| const [selectedGroup, setSelectedGroup] = useState(''); | |
| const logsEndRef = useRef<HTMLDivElement>(null); | |
| // Check initial authentication | |
| useEffect(() => { | |
| const savedPasskey = localStorage.getItem('passkey'); | |
| if (savedPasskey) { | |
| checkInitialAuth(savedPasskey); | |
| } | |
| }, []); | |
| const checkInitialAuth = async (key: string) => { | |
| try { | |
| const response = await fetch('/api/status', { | |
| headers: { 'x-passkey': key } | |
| }); | |
| if (response.ok) { | |
| setIsAuthenticated(true); | |
| } else { | |
| localStorage.removeItem('passkey'); | |
| } | |
| } catch (e) { | |
| console.error('Initial auth check failed:', e); | |
| } | |
| }; | |
| const handleLogin = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!passkey) return; | |
| setIsLoggingIn(true); | |
| setLoginError(''); | |
| try { | |
| const response = await fetch('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ passkey }) | |
| }); | |
| if (response.ok) { | |
| localStorage.setItem('passkey', passkey); | |
| setIsAuthenticated(true); | |
| setLoginError(''); | |
| } else { | |
| setLoginError('Invalid passkey. Please try again.'); | |
| } | |
| } catch (err) { | |
| setLoginError('Could not connect to server.'); | |
| } finally { | |
| setIsLoggingIn(false); | |
| } | |
| }; | |
| const handleLogout = () => { | |
| localStorage.removeItem('passkey'); | |
| setIsAuthenticated(false); | |
| setPasskey(''); | |
| }; | |
| useEffect(() => { | |
| if (!isAuthenticated) return; | |
| const pollStatus = async () => { | |
| const savedPasskey = localStorage.getItem('passkey') || ''; | |
| try { | |
| const response = await fetch('/api/status', { | |
| headers: { 'x-passkey': savedPasskey } | |
| }); | |
| if (response.status === 401) { | |
| handleLogout(); | |
| return; | |
| } | |
| if (response.ok) { | |
| const data = await response.json(); | |
| setStatus(data.status); | |
| setQrCode(data.qrCode); | |
| setLogs(data.logs); | |
| if (data.status === 'CONNECTED' && groups.length === 0) { | |
| fetchGroups(); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Polling error:', err); | |
| } | |
| }; | |
| pollStatus(); | |
| const interval = setInterval(pollStatus, 3000); | |
| return () => clearInterval(interval); | |
| }, [isAuthenticated, groups.length]); | |
| const fetchGroups = async () => { | |
| const savedPasskey = localStorage.getItem('passkey') || ''; | |
| try { | |
| const response = await fetch('/api/groups', { | |
| headers: { 'x-passkey': savedPasskey } | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| if (data.groups) setGroups(data.groups); | |
| } | |
| } catch (err) { | |
| console.error('Fetch groups error:', err); | |
| } | |
| }; | |
| const startService = async () => { | |
| const savedPasskey = localStorage.getItem('passkey') || ''; | |
| try { | |
| await fetch('/api/start', { | |
| method: 'POST', | |
| headers: { 'x-passkey': savedPasskey } | |
| }); | |
| } catch (err) { | |
| console.error('Start service error:', err); | |
| } | |
| }; | |
| const sendMessage = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setIsSending(true); | |
| const savedPasskey = localStorage.getItem('passkey') || ''; | |
| const recipient = activeTab === 'direct' ? phone : selectedGroup; | |
| try { | |
| const response = await fetch('/api/send', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-passkey': savedPasskey | |
| }, | |
| body: JSON.stringify({ | |
| phone: recipient, | |
| message, | |
| isGroup: activeTab === 'group' | |
| }) | |
| }); | |
| if (response.ok) { | |
| setMessage(''); | |
| alert('Message sent successfully!'); | |
| } else { | |
| const data = await response.json(); | |
| alert(`Error: ${data.error}`); | |
| } | |
| } catch (err) { | |
| alert(`Network error: ${err}`); | |
| } finally { | |
| setIsSending(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [logs]); | |
| if (!isAuthenticated) { | |
| return ( | |
| <div className="min-h-screen bg-[#0f172a] flex items-center justify-center p-4 font-sans"> | |
| <div className="max-w-md w-full bg-[#1e293b] rounded-3xl shadow-2xl border border-white/5 overflow-hidden"> | |
| <div className="p-8 text-center"> | |
| <div className="w-20 h-20 bg-orange-500/20 rounded-2xl flex items-center justify-center mx-auto mb-6 border border-orange-500/30 shadow-inner"> | |
| <ShieldCheck className="w-10 h-10 text-orange-500" /> | |
| </div> | |
| <h1 className="text-3xl font-bold text-white mb-2">Restricted Access</h1> | |
| <p className="text-slate-400 mb-8">Enter your security passkey to manage the WPPConnect server.</p> | |
| <form onSubmit={handleLogin} className="space-y-4"> | |
| <input | |
| type="password" | |
| placeholder="Security Passkey..." | |
| value={passkey} | |
| onChange={(e) => setPasskey(e.target.value)} | |
| className="w-full px-6 py-4 bg-slate-900/50 border border-slate-700 rounded-2xl focus:ring-4 focus:ring-orange-500/20 focus:border-orange-500 outline-none transition-all text-center text-xl font-mono tracking-widest text-white placeholder:tracking-normal placeholder:font-sans placeholder:text-slate-500" | |
| autoFocus | |
| /> | |
| {loginError && ( | |
| <div className="flex items-center justify-center gap-2 text-red-400 text-sm font-medium bg-red-500/10 py-3 rounded-xl border border-red-500/20"> | |
| <AlertCircle className="w-4 h-4" /> | |
| {loginError} | |
| </div> | |
| )} | |
| <button | |
| type="submit" | |
| disabled={isLoggingIn} | |
| className="w-full bg-orange-500 hover:bg-orange-600 disabled:bg-orange-500/50 text-white font-bold py-4 rounded-2xl transition-all shadow-lg shadow-orange-500/25 flex items-center justify-center gap-3 active:scale-[0.98]" | |
| > | |
| {isLoggingIn ? <RefreshCw className="w-5 h-5 animate-spin" /> : <Unlock className="w-5 h-5" />} | |
| Unlock Dashboard | |
| </button> | |
| </form> | |
| </div> | |
| <div className="bg-slate-900/50 py-4 text-center"> | |
| <p className="text-slate-500 text-xs font-semibold tracking-widest uppercase">Hugging Face Space Security</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen bg-[#f8f9fa] text-gray-900 font-sans selection:bg-orange-100 selection:text-orange-900"> | |
| <div className="max-w-6xl mx-auto px-4 py-8"> | |
| <header className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> | |
| <div className="flex items-center gap-4"> | |
| <div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-red-500 rounded-xl flex items-center justify-center shadow-lg shadow-orange-200"> | |
| <MessageSquare className="w-6 h-6 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="text-2xl font-bold tracking-tight text-gray-900">WPPConnect Portal</h1> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <div className={`w-2 h-2 rounded-full ${status === 'CONNECTED' ? 'bg-green-500' : 'bg-red-500'}`} /> | |
| <span className="text-xs font-semibold uppercase tracking-wider text-gray-500">{status}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| onClick={startService} | |
| disabled={status === 'CONNECTED' || status === 'INITIALIZING'} | |
| className="flex items-center gap-2 px-4 py-2.5 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-200 disabled:text-gray-400 text-white text-sm font-semibold rounded-xl transition-all shadow-md shadow-orange-100" | |
| > | |
| <Play className="w-4 h-4" /> | |
| Connect WhatsApp | |
| </button> | |
| <button | |
| onClick={handleLogout} | |
| className="p-2.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all" | |
| title="Logout" | |
| > | |
| <LogOut className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| </header> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <div className="space-y-6"> | |
| <div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6"> | |
| <div className="flex gap-1 p-1 bg-gray-50 rounded-xl mb-6"> | |
| <button | |
| onClick={() => setActiveTab('direct')} | |
| className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-semibold rounded-lg transition-all ${activeTab === 'direct' ? 'bg-white shadow-sm text-orange-600' : 'text-gray-500 hover:text-gray-700'}`} | |
| > | |
| <MessageSquare className="w-4 h-4" /> Direct | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('group')} | |
| className={`flex-1 flex items-center justify-center gap-2 py-2 text-sm font-semibold rounded-lg transition-all ${activeTab === 'group' ? 'bg-white shadow-sm text-orange-600' : 'text-gray-500 hover:text-gray-700'}`} | |
| > | |
| <Users className="w-4 h-4" /> Groups | |
| </button> | |
| </div> | |
| {status !== 'CONNECTED' ? ( | |
| <div className="flex flex-col items-center justify-center py-12 text-center space-y-4"> | |
| <div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center text-gray-300"> | |
| <QrCode className="w-8 h-8" /> | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-gray-900">WhatsApp Offline</h3> | |
| <p className="text-sm text-gray-500 max-w-[200px] mx-auto mt-1">Authenticate to enable messaging features.</p> | |
| </div> | |
| </div> | |
| ) : ( | |
| <form onSubmit={sendMessage} className="space-y-5"> | |
| {activeTab === 'direct' ? ( | |
| <div className="space-y-2"> | |
| <label className="block text-sm font-medium text-gray-700">Recipient Phone</label> | |
| <input | |
| type="text" | |
| placeholder="e.g. 5511999999999" | |
| value={phone} | |
| onChange={(e) => setPhone(e.target.value)} | |
| className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-all" | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| <label className="block text-sm font-medium text-gray-700">Select Group</label> | |
| <div className="flex gap-2"> | |
| <select | |
| value={selectedGroup} | |
| onChange={(e) => setSelectedGroup(e.target.value)} | |
| className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none" | |
| > | |
| <option value="">-- Choose a group --</option> | |
| {groups.map((g) => ( | |
| <option key={g.id._serialized} value={g.id._serialized}>{g.name || g.id.user}</option> | |
| ))} | |
| </select> | |
| <button | |
| type="button" | |
| onClick={fetchGroups} | |
| className="p-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" | |
| title="Refresh groups" | |
| > | |
| <RefreshCw className="w-4 h-4 text-gray-600" /> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| <div className="space-y-2"> | |
| <label className="block text-sm font-medium text-gray-700">Message</label> | |
| <textarea | |
| rows={4} | |
| placeholder="Type your message here..." | |
| value={message} | |
| onChange={(e) => setMessage(e.target.value)} | |
| className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none resize-none" | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={isSending || !message || (activeTab === 'direct' ? !phone : !selectedGroup)} | |
| className="w-full bg-gray-900 hover:bg-gray-800 disabled:bg-gray-300 text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center gap-2" | |
| > | |
| <Send className="w-5 h-5" /> | |
| {isSending ? 'Sending...' : 'Send Message'} | |
| </button> | |
| </form> | |
| )} | |
| </div> | |
| </div> | |
| <div className="space-y-6 flex flex-col"> | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex flex-col items-center justify-center min-h-[300px]"> | |
| <h2 className="text-lg font-semibold mb-4 border-b pb-2 w-full text-left text-gray-900">Authentication</h2> | |
| {status === 'QR_CODE' && qrCode ? ( | |
| <div className="flex flex-col items-center animate-in fade-in zoom-in duration-300"> | |
| <img src={qrCode} alt="WhatsApp QR Code" className="w-64 h-64 border-4 border-white shadow-sm rounded-lg" /> | |
| <p className="text-sm text-gray-500 mt-4 text-center">Scan this QR code with WhatsApp.</p> | |
| </div> | |
| ) : status === 'CONNECTED' ? ( | |
| <div className="flex flex-col items-center text-green-500"> | |
| <CheckCircle2 className="w-20 h-20 mb-4" /> | |
| <p className="font-medium text-gray-700">Authenticated & Ready</p> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col items-center text-gray-400"> | |
| <QrCode className="w-20 h-20 mb-4 opacity-50" /> | |
| <p className="text-sm">QR Code will appear here</p> | |
| </div> | |
| )} | |
| </div> | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200 flex flex-col flex-1 overflow-hidden"> | |
| <div className="p-4 border-b bg-gray-50 flex items-center gap-2"> | |
| <Terminal className="w-5 h-5 text-gray-500" /> | |
| <h2 className="text-sm font-semibold text-gray-700 text-gray-900">System Logs</h2> | |
| </div> | |
| <div className="bg-[#1e1e1e] text-[#d4d4d4] p-4 font-mono text-xs overflow-y-auto h-[300px] flex-1"> | |
| {logs.length === 0 ? ( | |
| <p className="text-gray-500 italic">Waiting for logs...</p> | |
| ) : ( | |
| logs.map((log, i) => ( | |
| <div key={i} className="mb-1 hover:bg-white/5 px-1 rounded"> | |
| {log} | |
| </div> | |
| )) | |
| )} | |
| <div ref={logsEndRef} /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; | |