Home.tsx
1 import { useState, useEffect } from "react"; 2 import ClientDashboard from "../components/ShieldedPool"; 3 import { DATA_URL } from "../components/lib/chart/data-url"; 4 5 const GovernanceDashboard = () => { 6 const [activeTab, setActiveTab] = useState< 7 "parameters" | "proposals" | "validator" | "charts" 8 >("parameters"); 9 const [darkMode, setDarkMode] = useState(false); 10 const [paramsData, setParamsData] = useState<any>(null); 11 const [propsData, setPropsData] = useState<any>(null); 12 const [validatorData, setValidatorData] = useState<any>(null); 13 const [loading, setLoading] = useState({ 14 params: true, 15 props: true, 16 validator: true, 17 }); 18 const [error, setError] = useState<{ 19 params: string | null; 20 props: string | null; 21 validator: string | null; 22 }>({ params: null, props: null, validator: null }); 23 24 useEffect(() => { 25 // Check for user's preferred color scheme 26 const prefersDark = window.matchMedia( 27 "(prefers-color-scheme: dark)" 28 ).matches; 29 setDarkMode(prefersDark); 30 31 // Fetch Parameters Data 32 const fetchParams = async () => { 33 try { 34 const response = await fetch(DATA_URL.protocol_parametersUrl); 35 if (!response.ok) 36 throw new Error(`HTTP error! status: ${response.status}`); 37 const jsonData = await response.json(); 38 setParamsData(jsonData[0]); 39 } catch (err) { 40 setError((prev) => ({ 41 ...prev, 42 params: 43 err instanceof Error ? err.message : "An unknown error occurred", 44 })); 45 } finally { 46 setLoading((prev) => ({ ...prev, params: false })); 47 } 48 }; 49 50 // Fetch Proposals Data 51 const fetchProps = async () => { 52 try { 53 // const response = await fetch( 54 // "https://raw.githubusercontent.com/ZecHub/zechub-wiki/main/public/data/namada/props.json" 55 // ); 56 const response = await fetch(DATA_URL.propsDetailsUrl); 57 if (!response.ok) 58 throw new Error(`HTTP error! status: ${response.status}`); 59 const jsonData = await response.json(); 60 setPropsData(jsonData[0]); 61 } catch (err) { 62 setError((prev) => ({ 63 ...prev, 64 props: 65 err instanceof Error ? err.message : "An unknown error occurred", 66 })); 67 } finally { 68 setLoading((prev) => ({ ...prev, props: false })); 69 } 70 }; 71 72 // Fetch Validator Data 73 const fetchValidator = async () => { 74 try { 75 const response = await fetch(DATA_URL.zechubUrl); 76 if (!response.ok) 77 throw new Error(`HTTP error! status: ${response.status}`); 78 const jsonData = await response.json(); 79 setValidatorData(jsonData[0]); 80 } catch (err) { 81 setError((prev) => ({ 82 ...prev, 83 validator: 84 err instanceof Error ? err.message : "An unknown error occurred", 85 })); 86 } finally { 87 setLoading((prev) => ({ ...prev, validator: false })); 88 } 89 }; 90 91 fetchParams(); 92 fetchProps(); 93 fetchValidator(); 94 }, []); 95 96 const toggleDarkMode = () => { 97 setDarkMode(!darkMode); 98 }; 99 100 if (loading.params && loading.props) { 101 return ( 102 <div 103 className={`flex justify-center items-center h-screen ${ 104 darkMode ? "bg-gray-900" : "bg-gray-100" 105 }`} 106 > 107 <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-400"></div> 108 </div> 109 ); 110 } 111 112 return ( 113 <div 114 className={`min-h-screen ${ 115 darkMode ? "dark bg-background" : "bg-background" 116 }`} 117 > 118 <div className="min-h-screen text-foreground"> 119 {/* Header Section */} 120 <header className="bg-card shadow-sm transition-theme fixed w-[100vw] z-[99]"> 121 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> 122 <div className="flex justify-between items-center"> 123 <div className="flex items-center space-x-4"> 124 <h1 className="text-2xl font-bold text-foreground"> 125 Namada Governance Dashboard 126 </h1> 127 </div> 128 <div className="flex items-center space-x-4"> 129 <button 130 onClick={toggleDarkMode} 131 className={`p-2 rounded-full transition-all duration-150 ${ 132 darkMode 133 ? "bg-gray-700 text-yellow-300 hover:bg-gray-600" 134 : "bg-gray-200 text-gray-700 hover:bg-gray-300" 135 }`} 136 aria-label="Toggle dark mode" 137 > 138 {darkMode ? ( 139 <span className="w-5 h-5 block">☀️</span> 140 ) : ( 141 <span className="w-5 h-5 block">🌙</span> 142 )} 143 </button> 144 </div> 145 </div> 146 </div> 147 </header> 148 149 {/* Main Content */} 150 <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 pt-[100px] imd:pt-[68px]"> 151 {/* Tabs Navigation */} 152 <div className="border-b border-border mb-6 transition-theme"> 153 <nav className="-mb-px flex space-x-8 flex-col imd:flex-row"> 154 <button 155 onClick={() => setActiveTab("parameters")} 156 className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm mr-0 imd:mr-8 ${ 157 activeTab === "parameters" 158 ? "text-blue-400 border-blue-400" 159 : `border-transparent ${ 160 darkMode 161 ? "hover:border-gray-300 text-gray-400 hover:text-gray-300" 162 : "hover:border-gray-300 text-gray-600 hover:text-gray-700" 163 }` 164 }`} 165 > 166 Protocol Parameters 167 </button> 168 <button 169 onClick={() => setActiveTab("proposals")} 170 className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm mr-0 imd:mr-8 ${ 171 activeTab === "proposals" 172 ? "text-blue-400 border-blue-400" 173 : `border-transparent ${ 174 darkMode 175 ? "hover:border-gray-300 text-gray-400 hover:text-gray-300" 176 : "hover:border-gray-300 text-gray-600 hover:text-gray-700" 177 }` 178 }`} 179 > 180 Governance Proposals 181 </button> 182 <button 183 onClick={() => setActiveTab("validator")} 184 className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm mr-0 imd:mr-8 ${ 185 activeTab === "validator" 186 ? "text-blue-400 border-blue-400" 187 : `border-transparent ${ 188 darkMode 189 ? "hover:border-gray-300 text-gray-400 hover:text-gray-300" 190 : "hover:border-gray-300 text-gray-600 hover:text-gray-700" 191 }` 192 }`} 193 > 194 Validator 195 </button> 196 <button 197 onClick={() => setActiveTab("charts")} 198 className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm ${ 199 activeTab === "charts" 200 ? "text-blue-400 border-blue-400" 201 : `border-transparent ${ 202 darkMode 203 ? "hover:border-gray-300 text-gray-400 hover:text-gray-300" 204 : "hover:border-gray-300 text-gray-600 hover:text-gray-700" 205 }` 206 }`} 207 > 208 Charts 209 </button> 210 </nav> 211 </div> 212 213 {/* Tab Content */} 214 {activeTab === "parameters" ? ( 215 <ParametersTab 216 data={paramsData} 217 loading={loading.params} 218 error={error.params} 219 darkMode={darkMode} 220 /> 221 ) : activeTab === "proposals" ? ( 222 <ProposalsTab 223 data={propsData} 224 loading={loading.props} 225 error={error.props} 226 darkMode={darkMode} 227 /> 228 ) : activeTab === "validator" ? ( 229 <ValidatorTab 230 data={validatorData} 231 loading={loading.validator} 232 error={error.validator} 233 darkMode={darkMode} 234 /> 235 ) : ( 236 <ClientDashboard /> 237 )} 238 </main> 239 </div> 240 </div> 241 ); 242 }; 243 244 const ParametersTab = ({ data, loading, error, darkMode }: any) => { 245 if (loading) { 246 return ( 247 <div className="flex justify-center items-center h-64"> 248 <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-400"></div> 249 </div> 250 ); 251 } 252 253 if (error) { 254 return ( 255 <div 256 className={`p-4 rounded-lg ${ 257 darkMode ? "bg-gray-800 text-red-400" : "bg-white text-red-600" 258 }`} 259 > 260 <strong>Error:</strong> {error} 261 </div> 262 ); 263 } 264 265 if (!data) return null; 266 267 return ( 268 <div className="space-y-6"> 269 {/* Governance Parameters Grid */} 270 <div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2"> 271 {/* Governance Parameters Card */} 272 <div 273 className={`rounded-lg shadow overflow-hidden ${ 274 darkMode ? "bg-gray-800" : "bg-white" 275 }`} 276 > 277 <div 278 className={`px-6 py-4 border-b ${ 279 darkMode ? "border-gray-700" : "border-gray-200" 280 }`} 281 > 282 <h2 283 className={`text-lg font-semibold ${ 284 darkMode ? "text-gray-200" : "text-gray-800" 285 }`} 286 > 287 Governance Parameters 288 </h2> 289 </div> 290 <div className="px-6 py-4"> 291 <div className="space-y-4"> 292 {Object.entries(data.Governance_Parameters[0]).map( 293 ([key, value]) => ( 294 <div key={key} className="flex justify-between"> 295 <span 296 className={`text-sm ${ 297 darkMode ? "text-gray-300" : "text-gray-600" 298 }`} 299 > 300 {key.replace(/_/g, " ")} 301 </span> 302 <span 303 className={`text-sm font-medium ${ 304 darkMode ? "text-gray-100" : "text-gray-900" 305 }`} 306 > 307 {typeof value === "number" 308 ? value.toLocaleString() 309 : String(value)} 310 </span> 311 </div> 312 ) 313 )} 314 </div> 315 </div> 316 </div> 317 318 {/* Public Goods Funding Card */} 319 <div 320 className={`rounded-lg shadow overflow-hidden ${ 321 darkMode ? "bg-gray-800" : "bg-white" 322 }`} 323 > 324 <div 325 className={`px-6 py-4 border-b ${ 326 darkMode ? "border-gray-700" : "border-gray-200" 327 }`} 328 > 329 <h2 330 className={`text-lg font-semibold ${ 331 darkMode ? "text-gray-200" : "text-gray-800" 332 }`} 333 > 334 Public Goods Funding 335 </h2> 336 </div> 337 <div className="px-6 py-4"> 338 <div className="space-y-4"> 339 {Object.entries(data.Public_Goods_Funding_Parameters[0]).map( 340 ([key, value]) => ( 341 <div key={key} className="flex justify-between"> 342 <span 343 className={`text-sm ${ 344 darkMode ? "text-gray-300" : "text-gray-600" 345 }`} 346 > 347 {key.replace(/_/g, " ")} 348 </span> 349 <span 350 className={`text-sm font-medium ${ 351 darkMode ? "text-gray-100" : "text-gray-900" 352 }`} 353 > 354 {typeof value === "number" ? `${value}` : String(value)} 355 </span> 356 </div> 357 ) 358 )} 359 </div> 360 </div> 361 </div> 362 363 {/* Protocol Parameters Card */} 364 <div 365 className={`rounded-lg shadow overflow-hidden ${ 366 darkMode ? "bg-gray-800" : "bg-white" 367 }`} 368 > 369 <div 370 className={`px-6 py-4 border-b ${ 371 darkMode ? "border-gray-700" : "border-gray-200" 372 }`} 373 > 374 <h2 375 className={`text-lg font-semibold ${ 376 darkMode ? "text-gray-200" : "text-gray-800" 377 }`} 378 > 379 Protocol Parameters 380 </h2> 381 </div> 382 <div className="px-6 py-4"> 383 <div className="space-y-4"> 384 {Object.entries(data.Protocol_Parameters[0]) 385 .filter( 386 ([key]) => 387 ![ 388 "VP_allowlist", 389 "Transactions_allowlist", 390 "Protocol_Parameters", 391 "Implicit_VP_hash", 392 ].includes(key) 393 ) 394 .map(([key, value]) => ( 395 <div key={key} className="flex justify-between"> 396 <span 397 className={`text-sm ${ 398 darkMode ? "text-gray-300" : "text-gray-600" 399 }`} 400 > 401 {key.replace(/_/g, " ")} 402 </span> 403 <span 404 className={`text-sm font-medium ${ 405 darkMode ? "text-gray-100" : "text-gray-900" 406 }`} 407 > 408 {typeof value === "boolean" 409 ? value 410 ? "Yes" 411 : "No" 412 : String(value)} 413 </span> 414 </div> 415 ))} 416 </div> 417 </div> 418 </div> 419 420 {/* Proof of Stake Card */} 421 <div 422 className={`rounded-lg shadow overflow-hidden ${ 423 darkMode ? "bg-gray-800" : "bg-white" 424 }`} 425 > 426 <div 427 className={`px-6 py-4 border-b ${ 428 darkMode ? "border-gray-700" : "border-gray-200" 429 }`} 430 > 431 <h2 432 className={`text-lg font-semibold ${ 433 darkMode ? "text-gray-200" : "text-gray-800" 434 }`} 435 > 436 Proof of Stake 437 </h2> 438 </div> 439 <div className="px-6 py-4"> 440 <div className="space-y-4"> 441 {Object.entries(data.Proof_Of_Stake_Parmeters[0]).map( 442 ([key, value]) => ( 443 <div key={key} className="flex justify-between"> 444 <span 445 className={`text-sm ${ 446 darkMode ? "text-gray-300" : "text-gray-600" 447 }`} 448 > 449 {key.replace(/_/g, " ")} 450 </span> 451 <span 452 className={`text-sm font-medium ${ 453 darkMode ? "text-gray-100" : "text-gray-900" 454 }`} 455 > 456 {typeof value === "number" && key.includes("rate") 457 ? `${value * 100}%` 458 : String(value)} 459 </span> 460 </div> 461 ) 462 )} 463 </div> 464 </div> 465 </div> 466 </div> 467 468 {/* Allowlists Section */} 469 <div 470 className={`rounded-lg shadow overflow-hidden ${ 471 darkMode ? "bg-gray-800" : "bg-white" 472 }`} 473 > 474 <div 475 className={`px-6 py-4 border-b ${ 476 darkMode ? "border-gray-700" : "border-gray-200" 477 }`} 478 > 479 <h2 480 className={`text-lg font-semibold ${ 481 darkMode ? "text-gray-200" : "text-gray-800" 482 }`} 483 > 484 Allowlists 485 </h2> 486 </div> 487 <div className="px-6 py-4"> 488 <div className="grid gap-6 md:grid-cols-2"> 489 {/* VP Allowlist */} 490 <div> 491 <h3 492 className={`text-md font-medium mb-3 ${ 493 darkMode ? "text-gray-300" : "text-gray-700" 494 }`} 495 > 496 VP Allowlist 497 </h3> 498 <div 499 className={`p-4 rounded-md max-h-60 overflow-y-auto ${ 500 darkMode ? "bg-gray-700" : "bg-gray-100" 501 }`} 502 > 503 {data.Protocol_Parameters[0].VP_allowlist.map( 504 (hash: string, i: number) => ( 505 <div 506 key={i} 507 className={`text-xs font-mono mb-1 break-all ${ 508 darkMode ? "text-gray-300" : "text-gray-700" 509 }`} 510 > 511 {hash} 512 </div> 513 ) 514 )} 515 </div> 516 <h3 517 className={`text-md font-medium mt-6 mb-3 ${ 518 darkMode ? "text-gray-300" : "text-gray-700" 519 }`} 520 > 521 Implicit VP hash 522 </h3> 523 <div 524 className={`p-4 rounded-md max-h-60 overflow-y-auto ${ 525 darkMode ? "bg-gray-700" : "bg-gray-100" 526 }`} 527 > 528 <div 529 className={`text-xs font-mono mb-1 break-all ${ 530 darkMode ? "text-gray-300" : "text-gray-700" 531 }`} 532 > 533 {data.Protocol_Parameters[0].Implicit_VP_hash} 534 </div> 535 </div> 536 </div> 537 538 {/* Transactions Allowlist */} 539 <div> 540 <h3 541 className={`text-md font-medium mb-3 ${ 542 darkMode ? "text-gray-300" : "text-gray-700" 543 }`} 544 > 545 Transactions Allowlist 546 </h3> 547 <div 548 className={`p-4 rounded-md max-h-60 overflow-y-auto ${ 549 darkMode ? "bg-gray-700" : "bg-gray-100" 550 }`} 551 > 552 {data.Protocol_Parameters[0].Transactions_allowlist.map( 553 (hash: string, i: number) => ( 554 <div 555 key={i} 556 className={`text-xs font-mono mb-1 break-all ${ 557 darkMode ? "text-gray-300" : "text-gray-700" 558 }`} 559 > 560 {hash} 561 </div> 562 ) 563 )} 564 </div> 565 </div> 566 </div> 567 </div> 568 </div> 569 </div> 570 ); 571 }; 572 573 const ProposalsTab = ({ data, loading, error, darkMode }: any) => { 574 const [expandedId, setExpandedId] = useState<number | null>(null); 575 576 const toggleExpand = (id: number) => { 577 setExpandedId(expandedId === id ? null : id); 578 }; 579 580 const parseResult = (resultString: string) => { 581 if (!resultString) return null; 582 583 const addressMatch = resultString.match(/(\d+)\s+addresses/); 584 const yaysMatch = resultString.match(/(\d+)\s+yays/); 585 const naysMatch = resultString.match(/(\d+)\s+nays/); 586 const abstainsMatch = resultString.match(/(\d+)\s+abstains/); 587 588 return { 589 addresses: addressMatch ? parseInt(addressMatch[1]) : 0, 590 yays: yaysMatch ? parseInt(yaysMatch[1]) : 0, 591 nays: naysMatch ? parseInt(naysMatch[1]) : 0, 592 abstains: abstainsMatch ? parseInt(abstainsMatch[1]) : 0, 593 }; 594 }; 595 596 if (loading) { 597 return ( 598 <div className="flex justify-center items-center h-64"> 599 <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-400"></div> 600 </div> 601 ); 602 } 603 604 if (error) { 605 return ( 606 <div 607 className={`p-4 rounded-lg ${ 608 darkMode ? "bg-gray-800 text-red-400" : "bg-white text-red-600" 609 }`} 610 > 611 <strong>Error:</strong> {error} 612 </div> 613 ); 614 } 615 616 if (!data) return null; 617 618 return ( 619 <div className="space-y-6"> 620 {/* Current Epoch */} 621 <div 622 className={`rounded-lg shadow px-6 py-4 ${ 623 darkMode ? "bg-gray-800" : "bg-white" 624 }`} 625 > 626 <h2 627 className={`text-lg font-semibold ${ 628 darkMode ? "text-gray-200" : "text-gray-800" 629 }`} 630 > 631 Current Epoch:{" "} 632 <span className="font-bold">{data.Last_committed_epoch}</span> 633 </h2> 634 </div> 635 636 {/* Proposals Table */} 637 <div 638 className={`rounded-lg shadow overflow-hidden ${ 639 darkMode ? "bg-gray-800" : "bg-white" 640 }`} 641 > 642 <div className="overflow-x-auto"> 643 <table 644 className={`min-w-full divide-y ${ 645 darkMode ? "divide-gray-700" : "divide-gray-200" 646 }`} 647 > 648 <thead className={darkMode ? "bg-gray-700" : "bg-gray-50"}> 649 <tr> 650 <th 651 scope="col" 652 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 653 darkMode ? "text-gray-300" : "text-gray-500" 654 }`} 655 > 656 {"#"} 657 </th> 658 <th 659 scope="col" 660 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 661 darkMode ? "text-gray-300" : "text-gray-500" 662 }`} 663 > 664 ID 665 </th> 666 <th 667 scope="col" 668 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 669 darkMode ? "text-gray-300" : "text-gray-500" 670 }`} 671 > 672 Type 673 </th> 674 <th 675 scope="col" 676 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 677 darkMode ? "text-gray-300" : "text-gray-500" 678 }`} 679 > 680 Author 681 </th> 682 <th 683 scope="col" 684 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 685 darkMode ? "text-gray-300" : "text-gray-500" 686 }`} 687 > 688 Start Epoch 689 </th> 690 <th 691 scope="col" 692 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 693 darkMode ? "text-gray-300" : "text-gray-500" 694 }`} 695 > 696 End Epoch 697 </th> 698 <th 699 scope="col" 700 className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider ${ 701 darkMode ? "text-gray-300" : "text-gray-500" 702 }`} 703 > 704 Activation Epoch 705 </th> 706 </tr> 707 </thead> 708 <tbody 709 className={`divide-y ${ 710 darkMode 711 ? "bg-gray-800 divide-gray-700" 712 : "bg-white divide-gray-200" 713 }`} 714 > 715 {data.Proposal.sort((a: any, b: any) => b.Id - a.Id).map( 716 (proposal: any, index: number) => { 717 const result = parseResult(proposal.Result); 718 const totalVotes = result 719 ? result.yays + result.nays + result.abstains 720 : 0; 721 const yayPercentage = 722 result && totalVotes > 0 723 ? (result.yays / totalVotes) * 100 724 : 0; 725 const nayPercentage = 726 result && totalVotes > 0 727 ? (result.nays / totalVotes) * 100 728 : 0; 729 const abstainPercentage = 730 result && totalVotes > 0 731 ? (result.abstains / totalVotes) * 100 732 : 0; 733 734 return ( 735 <> 736 <tr 737 key={proposal.Id} 738 onClick={() => toggleExpand(proposal.Id)} 739 className={`cursor-pointer ${ 740 darkMode ? "hover:bg-gray-700" : "hover:bg-gray-50" 741 }`} 742 > 743 <td 744 className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${ 745 darkMode ? "text-gray-100" : "text-gray-900" 746 }`} 747 > 748 {index + 1} 749 </td> 750 <td 751 className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${ 752 darkMode ? "text-gray-100" : "text-gray-900" 753 }`} 754 > 755 {proposal.Id} 756 </td> 757 <td 758 className={`px-6 py-4 whitespace-nowrap text-sm ${ 759 darkMode ? "text-gray-300" : "text-gray-600" 760 }`} 761 > 762 {proposal.Type} 763 </td> 764 <td 765 className={`px-6 py-4 whitespace-nowrap text-sm font-mono ${ 766 darkMode ? "text-gray-300" : "text-gray-600" 767 }`} 768 > 769 {proposal.Author} 770 </td> 771 <td 772 className={`px-6 py-4 whitespace-nowrap text-sm ${ 773 darkMode ? "text-gray-300" : "text-gray-600" 774 }`} 775 > 776 {proposal.Start_Epoch} 777 </td> 778 <td 779 className={`px-6 py-4 whitespace-nowrap text-sm ${ 780 darkMode ? "text-gray-300" : "text-gray-600" 781 }`} 782 > 783 {proposal.End_Epoch} 784 </td> 785 <td 786 className={`px-6 py-4 whitespace-nowrap text-sm ${ 787 darkMode ? "text-gray-300" : "text-gray-600" 788 }`} 789 > 790 {proposal.Activation_Epoch} 791 </td> 792 </tr> 793 {expandedId === proposal.Id && ( 794 <tr key={`content-${proposal.Id}`}> 795 <td 796 colSpan={7} 797 className={`px-6 py-6 max-w-[73rem] ${ 798 darkMode ? "bg-gray-900" : "bg-gray-50" 799 }`} 800 > 801 {/* Proposal Content */} 802 <div className="space-y-4"> 803 {proposal.Content && 804 Object.keys(proposal.Content) 805 .reverse() 806 .map((key) => ( 807 <div key={key}> 808 <div 809 className={`font-semibold text-sm uppercase mb-1 ${ 810 darkMode 811 ? "text-gray-400" 812 : "text-gray-600" 813 }`} 814 > 815 {key.replace(/-/g, " ")} 816 </div> 817 <div 818 className={`text-sm whitespace-pre-wrap ${ 819 darkMode 820 ? "text-gray-300" 821 : "text-gray-700" 822 }`} 823 > 824 {proposal.Content[key]} 825 </div> 826 </div> 827 ))} 828 </div> 829 {/* Voting Results */} 830 {result && ( 831 <div 832 className={`mt-6 p-4 rounded-lg ${ 833 darkMode ? "bg-gray-800" : "bg-white" 834 }`} 835 > 836 <h3 837 className={`text-lg font-semibold mb-4 ${ 838 darkMode ? "text-gray-200" : "text-gray-800" 839 }`} 840 > 841 Voting Results 842 </h3> 843 844 {/* Total Addresses */} 845 <div className="mb-4"> 846 <span 847 className={`text-sm ${ 848 darkMode 849 ? "text-gray-400" 850 : "text-gray-600" 851 }`} 852 > 853 Total Addresses: 854 </span> 855 <span 856 className={`ml-2 text-sm font-semibold ${ 857 darkMode 858 ? "text-gray-200" 859 : "text-gray-800" 860 }`} 861 > 862 {result.addresses.toLocaleString()} 863 </span> 864 </div> 865 866 {/* Vote Breakdown */} 867 <div className="space-y-3"> 868 {/* Yays */} 869 <div> 870 <div className="flex justify-between mb-1"> 871 <span 872 className={`text-sm font-medium ${ 873 darkMode 874 ? "text-green-400" 875 : "text-green-600" 876 }`} 877 > 878 Yays 879 </span> 880 <span 881 className={`text-sm font-semibold ${ 882 darkMode 883 ? "text-green-400" 884 : "text-green-600" 885 }`} 886 > 887 {result.yays.toLocaleString()} ( 888 {yayPercentage.toFixed(1)}%) 889 </span> 890 </div> 891 <div 892 className={`w-full rounded-full h-2.5 ${ 893 darkMode ? "bg-gray-700" : "bg-gray-200" 894 }`} 895 > 896 <div 897 className="bg-green-500 h-2.5 rounded-full" 898 style={{ width: `${yayPercentage}%` }} 899 ></div> 900 </div> 901 </div> 902 903 {/* Nays */} 904 <div> 905 <div className="flex justify-between mb-1"> 906 <span 907 className={`text-sm font-medium ${ 908 darkMode 909 ? "text-red-400" 910 : "text-red-600" 911 }`} 912 > 913 Nays 914 </span> 915 <span 916 className={`text-sm font-semibold ${ 917 darkMode 918 ? "text-red-400" 919 : "text-red-600" 920 }`} 921 > 922 {result.nays.toLocaleString()} ( 923 {nayPercentage.toFixed(1)}%) 924 </span> 925 </div> 926 <div 927 className={`w-full rounded-full h-2.5 ${ 928 darkMode ? "bg-gray-700" : "bg-gray-200" 929 }`} 930 > 931 <div 932 className="bg-red-500 h-2.5 rounded-full" 933 style={{ width: `${nayPercentage}%` }} 934 ></div> 935 </div> 936 </div> 937 938 {/* Abstains */} 939 <div> 940 <div className="flex justify-between mb-1"> 941 <span 942 className={`text-sm font-medium ${ 943 darkMode 944 ? "text-gray-400" 945 : "text-gray-600" 946 }`} 947 > 948 Abstains 949 </span> 950 <span 951 className={`text-sm font-semibold ${ 952 darkMode 953 ? "text-gray-400" 954 : "text-gray-600" 955 }`} 956 > 957 {result.abstains.toLocaleString()} ( 958 {abstainPercentage.toFixed(1)}%) 959 </span> 960 </div> 961 <div 962 className={`w-full rounded-full h-2.5 ${ 963 darkMode ? "bg-gray-700" : "bg-gray-200" 964 }`} 965 > 966 <div 967 className={`h-2.5 rounded-full ${ 968 darkMode 969 ? "bg-gray-500" 970 : "bg-gray-400" 971 }`} 972 style={{ 973 width: `${abstainPercentage}%`, 974 }} 975 ></div> 976 </div> 977 </div> 978 </div> 979 </div> 980 )} 981 </td> 982 </tr> 983 )} 984 </> 985 ); 986 } 987 )} 988 </tbody> 989 </table> 990 </div> 991 </div> 992 </div> 993 ); 994 }; 995 996 const ValidatorTab = ({ data, loading, error, darkMode }: any) => { 997 if (loading) { 998 return ( 999 <div className="flex justify-center items-center h-64"> 1000 <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-400"></div> 1001 </div> 1002 ); 1003 } 1004 1005 if (error) { 1006 return ( 1007 <div 1008 className={`p-4 rounded-lg ${ 1009 darkMode ? "bg-gray-800 text-red-400" : "bg-white text-red-600" 1010 }`} 1011 > 1012 <strong>Error:</strong> {error} 1013 </div> 1014 ); 1015 } 1016 1017 if (!data) return null; 1018 1019 return ( 1020 <div className="space-y-6"> 1021 <div 1022 className={`rounded-xl shadow-2xl overflow-hidden w-full max-w-2xl ${ 1023 darkMode ? "bg-gray-800" : "bg-white" 1024 }`} 1025 > 1026 {/* Header with Avatar */} 1027 <div 1028 className={`p-6 flex items-center space-x-4 ${ 1029 darkMode ? "bg-gray-700" : "bg-gray-100" 1030 }`} 1031 > 1032 <img 1033 src={data.Avatar} 1034 alt="ZecHub Logo" 1035 className="w-16 h-16 rounded-full border-2 border-blue-500" 1036 /> 1037 <div> 1038 <h1 className="text-2xl font-bold text-blue-400">{data.Name}</h1> 1039 <p className={darkMode ? "text-gray-300" : "text-gray-600"}> 1040 {data.Description} 1041 </p> 1042 </div> 1043 </div> 1044 1045 {/* Main Content */} 1046 <div className="p-6 space-y-4"> 1047 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 1048 <div 1049 className={`p-4 rounded-lg ${ 1050 darkMode ? "bg-gray-700" : "bg-gray-100" 1051 }`} 1052 > 1053 <h2 1054 className={`text-sm font-semibold uppercase tracking-wider ${ 1055 darkMode ? "text-gray-400" : "text-gray-500" 1056 }`} 1057 > 1058 Contact 1059 </h2> 1060 <p className="mt-2"> 1061 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1062 Email: 1063 </span>{" "} 1064 {data.Email} 1065 </p> 1066 <p className="mt-1"> 1067 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1068 Discord: 1069 </span>{" "} 1070 {data.Discord} 1071 </p> 1072 <p className="mt-1"> 1073 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1074 Website: 1075 </span> 1076 <a 1077 href={data.Website} 1078 target="_blank" 1079 rel="noopener noreferrer" 1080 className="text-blue-400 hover:underline ml-1" 1081 > 1082 {data.Website} 1083 </a> 1084 </p> 1085 </div> 1086 1087 <div 1088 className={`p-4 rounded-lg ${ 1089 darkMode ? "bg-gray-700" : "bg-gray-100" 1090 }`} 1091 > 1092 <h2 1093 className={`text-sm font-semibold uppercase tracking-wider ${ 1094 darkMode ? "text-gray-400" : "text-gray-500" 1095 }`} 1096 > 1097 Staking Details 1098 </h2> 1099 <p className="mt-2"> 1100 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1101 Commission: 1102 </span>{" "} 1103 {data.Commission} 1104 </p> 1105 <p className="mt-1"> 1106 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1107 Max Change: 1108 </span>{" "} 1109 {data.Max_Change} 1110 </p> 1111 <p className="mt-1"> 1112 <span className={darkMode ? "text-gray-400" : "text-gray-500"}> 1113 Epoch: 1114 </span>{" "} 1115 {data.Epoch} 1116 </p> 1117 </div> 1118 </div> 1119 1120 <div 1121 className={`p-4 rounded-lg ${ 1122 darkMode ? "bg-gray-700" : "bg-gray-100" 1123 }`} 1124 > 1125 <h2 1126 className={`text-sm font-semibold uppercase tracking-wider ${ 1127 darkMode ? "text-gray-400" : "text-gray-500" 1128 }`} 1129 > 1130 Address 1131 </h2> 1132 <p 1133 className={`mt-2 font-mono text-sm break-all p-3 rounded ${ 1134 darkMode ? "bg-gray-800" : "bg-gray-200" 1135 }`} 1136 > 1137 {data.Address} 1138 </p> 1139 </div> 1140 </div> 1141 </div> 1142 </div> 1143 ); 1144 }; 1145 1146 export default GovernanceDashboard;