|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { useEffect, useState } from 'react'; |
| 4 | +import Link from 'next/link'; |
| 5 | +import { getChannels, PaymentChannel } from '@/lib/payment-channel'; |
| 6 | +import { ChannelCard } from '@/components/channels/ChannelCard'; |
| 7 | +import { ChannelDetail } from '@/components/channels/ChannelDetail'; |
| 8 | +import { OpenChannelModal } from '@/components/channels/OpenChannelModal'; |
| 9 | +import { Button } from '@/components/ui/button'; |
| 10 | + |
| 11 | +export default function ChannelsPage() { |
| 12 | + const [channels, setChannels] = useState<PaymentChannel[]>([]); |
| 13 | + const [selectedChannel, setSelectedChannel] = useState<PaymentChannel | null>(null); |
| 14 | + const [isModalOpen, setIsModalOpen] = useState(false); |
| 15 | + const [isLoading, setIsLoading] = useState(true); |
| 16 | + |
| 17 | + const loadChannels = async () => { |
| 18 | + try { |
| 19 | + const data = await getChannels(); |
| 20 | + setChannels(data); |
| 21 | + } catch (err) { |
| 22 | + console.error(err); |
| 23 | + } finally { |
| 24 | + setIsLoading(false); |
| 25 | + } |
| 26 | + }; |
| 27 | + |
| 28 | + useEffect(() => { |
| 29 | + loadChannels(); |
| 30 | + // Poll for updates every 10 seconds |
| 31 | + const interval = setInterval(loadChannels, 10000); |
| 32 | + return () => clearInterval(interval); |
| 33 | + }, []); |
| 34 | + |
| 35 | + const handleChannelOpened = (channel: PaymentChannel) => { |
| 36 | + setChannels((prev) => [...prev, channel]); |
| 37 | + }; |
| 38 | + |
| 39 | + const handleChannelUpdate = (updated: PaymentChannel) => { |
| 40 | + setChannels((prev) => prev.map((c) => (c.id === updated.id ? updated : c))); |
| 41 | + setSelectedChannel(updated); |
| 42 | + }; |
| 43 | + |
| 44 | + const hasLowBalance = channels.some((c) => parseFloat(c.balance) < 10); |
| 45 | + const hasExpiring = channels.some((c) => c.expiry && new Date(c.expiry) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)); |
| 46 | + |
| 47 | + return ( |
| 48 | + <main className="min-h-screen bg-gray-50 py-12 px-4"> |
| 49 | + <div className="max-w-4xl mx-auto"> |
| 50 | + <div className="flex justify-between items-center mb-8"> |
| 51 | + <div> |
| 52 | + <Link |
| 53 | + href="/settings/privacy" |
| 54 | + className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700 mb-4 transition-colors" |
| 55 | + > |
| 56 | + <svg className="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
| 57 | + <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /> |
| 58 | + </svg> |
| 59 | + Back to Privacy & Data |
| 60 | + </Link> |
| 61 | + <h1 className="text-2xl font-semibold text-gray-900 mb-1">Payment Channels</h1> |
| 62 | + <p className="text-sm text-gray-500">Open, monitor, and manage your payment channels.</p> |
| 63 | + </div> |
| 64 | + <Button onClick={() => setIsModalOpen(true)}>Open Channel</Button> |
| 65 | + </div> |
| 66 | + |
| 67 | + {(hasLowBalance || hasExpiring) && ( |
| 68 | + <div className="mb-6 space-y-3"> |
| 69 | + {hasLowBalance && ( |
| 70 | + <div className="flex items-start gap-2 text-sm text-yellow-700 bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-3"> |
| 71 | + <svg className="w-4 h-4 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
| 72 | + <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /> |
| 73 | + </svg> |
| 74 | + <span>One or more channels have low balances. Consider topping up to avoid service interruptions.</span> |
| 75 | + </div> |
| 76 | + )} |
| 77 | + {hasExpiring && ( |
| 78 | + <div className="flex items-start gap-2 text-sm text-orange-700 bg-orange-50 border border-orange-200 rounded-lg px-4 py-3"> |
| 79 | + <svg className="w-4 h-4 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
| 80 | + <path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 81 | + </svg> |
| 82 | + <span>One or more channels are expiring soon. Close or renew them to avoid disputes.</span> |
| 83 | + </div> |
| 84 | + )} |
| 85 | + </div> |
| 86 | + )} |
| 87 | + |
| 88 | + {selectedChannel ? ( |
| 89 | + <ChannelDetail |
| 90 | + channel={selectedChannel} |
| 91 | + onBack={() => setSelectedChannel(null)} |
| 92 | + onUpdate={handleChannelUpdate} |
| 93 | + /> |
| 94 | + ) : ( |
| 95 | + <> |
| 96 | + {isLoading ? ( |
| 97 | + <div className="grid gap-4 md:grid-cols-2"> |
| 98 | + {[1, 2].map((i) => ( |
| 99 | + <div key={i} className="bg-white rounded-2xl border border-gray-200 shadow-sm p-6 animate-pulse"> |
| 100 | + <div className="h-6 bg-gray-200 rounded w-1/2 mb-2" /> |
| 101 | + <div className="h-4 bg-gray-200 rounded w-1/3 mb-6" /> |
| 102 | + <div className="h-8 bg-gray-200 rounded w-1/4 mb-2" /> |
| 103 | + <div className="h-4 bg-gray-200 rounded w-2/3" /> |
| 104 | + </div> |
| 105 | + ))} |
| 106 | + </div> |
| 107 | + ) : channels.length === 0 ? ( |
| 108 | + <div className="text-center py-12"> |
| 109 | + <h3 className="text-lg font-medium text-gray-900 mb-2">No channels yet</h3> |
| 110 | + <p className="text-sm text-gray-500 mb-6">Open a payment channel to get started.</p> |
| 111 | + <Button onClick={() => setIsModalOpen(true)}>Open Channel</Button> |
| 112 | + </div> |
| 113 | + ) : ( |
| 114 | + <div className="grid gap-4 md:grid-cols-2"> |
| 115 | + {channels.map((channel) => ( |
| 116 | + <ChannelCard |
| 117 | + key={channel.id} |
| 118 | + channel={channel} |
| 119 | + onSelect={setSelectedChannel} |
| 120 | + /> |
| 121 | + ))} |
| 122 | + </div> |
| 123 | + )} |
| 124 | + </> |
| 125 | + )} |
| 126 | + </div> |
| 127 | + |
| 128 | + <OpenChannelModal |
| 129 | + isOpen={isModalOpen} |
| 130 | + onClose={() => setIsModalOpen(false)} |
| 131 | + onChannelOpened={handleChannelOpened} |
| 132 | + /> |
| 133 | + </main> |
| 134 | + ); |
| 135 | +} |
0 commit comments