mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-15 13:01:05 +00:00
feat: nav 4x2 button grid + hero stats from correct chains
- Replace nav menu with 4x2 mobile-app-style button grid (responsive) - Fetch staking stats from Asset Hub instead of Relay Chain - Governance stats (proposals, voters) remain on Relay Chain - Trust score: show login prompt for guests, real score for users - Full-width root container, centered hero, node globals shim fix
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 153 KiB |
+2
-4
@@ -1,8 +1,6 @@
|
|||||||
#root {
|
#root {
|
||||||
max-width: 1280px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
min-height: 100vh;
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
+168
-192
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@@ -19,7 +19,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview';
|
|||||||
import { FundingProposal } from './treasury/FundingProposal';
|
import { FundingProposal } from './treasury/FundingProposal';
|
||||||
import { SpendingHistory } from './treasury/SpendingHistory';
|
import { SpendingHistory } from './treasury/SpendingHistory';
|
||||||
import { MultiSigApproval } from './treasury/MultiSigApproval';
|
import { MultiSigApproval } from './treasury/MultiSigApproval';
|
||||||
import { ExternalLink, Award, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, Users, Droplet, Mail, Coins, Menu, X } from 'lucide-react';
|
import { ExternalLink, Award, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, Users, Droplet, Mail, Coins, Menu, X, ChevronDown } from 'lucide-react';
|
||||||
import GovernanceInterface from './GovernanceInterface';
|
import GovernanceInterface from './GovernanceInterface';
|
||||||
import RewardDistribution from './RewardDistribution';
|
import RewardDistribution from './RewardDistribution';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
@@ -48,11 +48,26 @@ const AppLayout: React.FC = () => {
|
|||||||
const [showEducation, setShowEducation] = useState(false);
|
const [showEducation, setShowEducation] = useState(false);
|
||||||
const [showP2P, setShowP2P] = useState(false);
|
const [showP2P, setShowP2P] = useState(false);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||||
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isConnected } = useWebSocket();
|
const { isConnected } = useWebSocket();
|
||||||
useWallet();
|
useWallet();
|
||||||
const [, _setIsAdmin] = useState(false);
|
const [, _setIsAdmin] = useState(false);
|
||||||
|
|
||||||
|
// Close dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (gridRef.current && !gridRef.current.contains(e.target as Node)) {
|
||||||
|
setOpenMenu(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (openMenu) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [openMenu]);
|
||||||
|
|
||||||
// Admin status is handled by AuthContext via wallet whitelist
|
// Admin status is handled by AuthContext via wallet whitelist
|
||||||
// Supabase admin_roles is optional (table may not exist)
|
// Supabase admin_roles is optional (table may not exist)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -62,7 +77,7 @@ const AppLayout: React.FC = () => {
|
|||||||
<div className="min-h-screen bg-gray-950 text-white">
|
<div className="min-h-screen bg-gray-950 text-white">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="fixed top-0 w-full z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
|
<nav className="fixed top-0 w-full z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
|
||||||
<div className="w-full px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className="flex items-center justify-between h-16">
|
||||||
{/* LEFT: Logo - hidden on mobile */}
|
{/* LEFT: Logo - hidden on mobile */}
|
||||||
<div className="flex-shrink-0 hidden lg:block">
|
<div className="flex-shrink-0 hidden lg:block">
|
||||||
@@ -83,211 +98,172 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CENTER & RIGHT: Menu + Actions in same row (Desktop) */}
|
{/* CENTER & RIGHT: Menu + Actions in same row (Desktop) */}
|
||||||
<div className="hidden lg:flex items-center space-x-4 flex-1 justify-start ml-8 pr-4">
|
<div className="hidden lg:flex items-center gap-2 xl:gap-4 flex-1 justify-between ml-4 xl:ml-8 min-w-0">
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<div />
|
||||||
<button
|
|
||||||
onClick={() => navigate('/dashboard')}
|
|
||||||
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<LayoutDashboard className="w-4 h-4" />
|
|
||||||
Dashboard
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/wallet')}
|
|
||||||
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<Wallet className="w-4 h-4" />
|
|
||||||
Wallet
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/be-citizen')}
|
|
||||||
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold"
|
|
||||||
>
|
|
||||||
<Users className="w-4 h-4" />
|
|
||||||
Be Citizen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Governance Dropdown */}
|
|
||||||
<div className="relative group">
|
|
||||||
<button className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm">
|
|
||||||
<FileEdit className="w-4 h-4" />
|
|
||||||
Governance
|
|
||||||
<svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div className="absolute left-0 mt-2 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowProposalWizard(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
|
||||||
>
|
|
||||||
<FileEdit className="w-4 h-4" />
|
|
||||||
Proposals
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDelegation(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Users2 className="w-4 h-4" />
|
|
||||||
Delegation
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowForum(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-4 h-4" />
|
|
||||||
Forum
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowTreasury(true);
|
|
||||||
setTreasuryTab('overview');
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<PiggyBank className="w-4 h-4" />
|
|
||||||
Treasury
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowModeration(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg"
|
|
||||||
>
|
|
||||||
<ShieldCheck className="w-4 h-4" />
|
|
||||||
Moderation
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Trading Dropdown */}
|
|
||||||
<div className="relative group">
|
|
||||||
<button className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm">
|
|
||||||
<ArrowRightLeft className="w-4 h-4" />
|
|
||||||
Trading
|
|
||||||
<svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div className="absolute left-0 mt-2 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDEX(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
|
||||||
>
|
|
||||||
<Droplet className="w-4 h-4" />
|
|
||||||
DEX Pools
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowP2P(true);
|
|
||||||
navigate('/p2p');
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Users className="w-4 h-4" />
|
|
||||||
P2P
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/presale')}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Coins className="w-4 h-4" />
|
|
||||||
Presale
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowStaking(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<TrendingUp className="w-4 h-4" />
|
|
||||||
Staking
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowMultiSig(true)}
|
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg"
|
|
||||||
>
|
|
||||||
<Lock className="w-4 h-4" />
|
|
||||||
MultiSig
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowEducation(true);
|
|
||||||
navigate('/education');
|
|
||||||
}}
|
|
||||||
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<Award className="w-4 h-4" />
|
|
||||||
Education
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/profile/settings')}
|
|
||||||
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
await signOut();
|
|
||||||
navigate('/login');
|
|
||||||
}}
|
|
||||||
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<LogIn className="w-4 h-4 rotate-180" />
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex items-center gap-2 flex-shrink min-w-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/be-citizen')}
|
onClick={() => navigate('/be-citizen')}
|
||||||
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold"
|
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<Users className="w-4 h-4" />
|
<Users className="w-4 h-4" />
|
||||||
Be Citizen
|
Be Citizen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/login')}
|
onClick={() => navigate('/login')}
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<a
|
{/* Right actions - always visible, flex-shrink-0 */}
|
||||||
href="/docs"
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
className="text-gray-300 hover:text-white transition-colors text-sm"
|
<a
|
||||||
>
|
href="/docs"
|
||||||
Docs
|
className="text-gray-300 hover:text-white transition-colors text-sm"
|
||||||
</a>
|
>
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Divider */}
|
<div className="w-px h-6 bg-gray-700"></div>
|
||||||
<div className="w-px h-6 bg-gray-700"></div>
|
|
||||||
|
|
||||||
{/* Actions continue after Docs */}
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
{isConnected ? (
|
||||||
{isConnected ? (
|
<Wifi className="w-4 h-4 text-green-500" />
|
||||||
<Wifi className="w-4 h-4 text-green-500" />
|
) : (
|
||||||
) : (
|
<WifiOff className="w-4 h-4 text-gray-500" />
|
||||||
<WifiOff className="w-4 h-4 text-gray-500" />
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<NotificationBell />
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<PezkuwiWalletButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NotificationBell />
|
|
||||||
<LanguageSwitcher />
|
|
||||||
<PezkuwiWalletButton />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Button Grid (logged in only) */}
|
||||||
|
{user && (
|
||||||
|
<div ref={gridRef} className="fixed top-16 w-full z-30 bg-gray-950/95 backdrop-blur-md border-b border-gray-800">
|
||||||
|
<div className="max-w-5xl mx-auto grid grid-cols-4 gap-1.5 sm:gap-2 px-2 sm:px-4 py-1.5 sm:py-2">
|
||||||
|
{/* Dashboard */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setOpenMenu(null); navigate('/dashboard'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-green-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<LayoutDashboard className="w-4 h-4 sm:w-5 sm:h-5 text-green-400" />
|
||||||
|
Dashboard
|
||||||
|
</button>
|
||||||
|
{/* Wallet */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setOpenMenu(null); navigate('/wallet'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-yellow-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<Wallet className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" />
|
||||||
|
Wallet
|
||||||
|
</button>
|
||||||
|
{/* Be Citizen */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setOpenMenu(null); navigate('/be-citizen'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-cyan-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4 sm:w-5 sm:h-5 text-cyan-400" />
|
||||||
|
Be Citizen
|
||||||
|
</button>
|
||||||
|
{/* Governance (dropdown) */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenMenu(openMenu === 'governance' ? null : 'governance')}
|
||||||
|
className="w-full flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-green-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<FileEdit className="w-4 h-4 sm:w-5 sm:h-5 text-green-400" />
|
||||||
|
<span className="flex items-center gap-0.5">Governance <ChevronDown className="w-3 h-3" /></span>
|
||||||
|
</button>
|
||||||
|
{openMenu === 'governance' && (
|
||||||
|
<div className="absolute left-0 top-full mt-1 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg z-50">
|
||||||
|
<button onClick={() => { setShowProposalWizard(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg">
|
||||||
|
<FileEdit className="w-4 h-4" /> Proposals
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowDelegation(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<Users2 className="w-4 h-4" /> Delegation
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowForum(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-4 h-4" /> Forum
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowTreasury(true); setTreasuryTab('overview'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<PiggyBank className="w-4 h-4" /> Treasury
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowModeration(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg">
|
||||||
|
<ShieldCheck className="w-4 h-4" /> Moderation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Trading (dropdown) */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenMenu(openMenu === 'trading' ? null : 'trading')}
|
||||||
|
className="w-full flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-red-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<ArrowRightLeft className="w-4 h-4 sm:w-5 sm:h-5 text-red-400" />
|
||||||
|
<span className="flex items-center gap-0.5">Trading <ChevronDown className="w-3 h-3" /></span>
|
||||||
|
</button>
|
||||||
|
{openMenu === 'trading' && (
|
||||||
|
<div className="absolute left-0 top-full mt-1 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg z-50">
|
||||||
|
<button onClick={() => { setShowDEX(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg">
|
||||||
|
<Droplet className="w-4 h-4" /> DEX Pools
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowP2P(true); navigate('/p2p'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4" /> P2P
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { navigate('/presale'); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<Coins className="w-4 h-4" /> Presale
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowStaking(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4" /> Staking
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { setShowMultiSig(true); setOpenMenu(null); }} className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg">
|
||||||
|
<Lock className="w-4 h-4" /> MultiSig
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Education */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setOpenMenu(null); setShowEducation(true); navigate('/education'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-yellow-400/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<Award className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" />
|
||||||
|
Education
|
||||||
|
</button>
|
||||||
|
{/* Settings */}
|
||||||
|
<button
|
||||||
|
onClick={() => { setOpenMenu(null); navigate('/profile/settings'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-gray-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400" />
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
{/* Logout */}
|
||||||
|
<button
|
||||||
|
onClick={async () => { setOpenMenu(null); await signOut(); navigate('/login'); }}
|
||||||
|
className="flex flex-col items-center gap-0.5 sm:gap-1 p-1.5 sm:p-2 rounded-xl bg-gray-900/70 border border-red-500/40 text-[10px] sm:text-xs font-medium transition-all hover:scale-[1.03] active:scale-95 cursor-pointer text-gray-300 hover:text-white"
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4 sm:w-5 sm:h-5 text-red-400 rotate-180" />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile Menu Panel */}
|
{/* Mobile Menu Panel */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="fixed inset-0 z-30 lg:hidden">
|
<div className="fixed inset-0 z-30 lg:hidden">
|
||||||
@@ -432,13 +408,13 @@ const AppLayout: React.FC = () => {
|
|||||||
<main>
|
<main>
|
||||||
{/* Conditional Rendering for Features */}
|
{/* Conditional Rendering for Features */}
|
||||||
{showDEX ? (
|
{showDEX ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<DEXDashboard />
|
<DEXDashboard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showProposalWizard ? (
|
) : showProposalWizard ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<ProposalWizard
|
<ProposalWizard
|
||||||
onComplete={(proposal) => {
|
onComplete={(proposal) => {
|
||||||
@@ -450,25 +426,25 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showDelegation ? (
|
) : showDelegation ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<DelegationManager />
|
<DelegationManager />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showForum ? (
|
) : showForum ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<ForumOverview />
|
<ForumOverview />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showModeration ? (
|
) : showModeration ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<ModerationPanel />
|
<ModerationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showTreasury ? (
|
) : showTreasury ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||||
@@ -518,7 +494,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showStaking ? (
|
) : showStaking ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||||
@@ -532,7 +508,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showMultiSig ? (
|
) : showMultiSig ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<div className="max-w-full mx-auto px-4">
|
<div className="max-w-full mx-auto px-4">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||||
@@ -546,11 +522,11 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showEducation ? (
|
) : showEducation ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<EducationPlatform />
|
<EducationPlatform />
|
||||||
</div>
|
</div>
|
||||||
) : showP2P ? (
|
) : showP2P ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<div className="pt-[8rem] sm:pt-[8.5rem] min-h-screen bg-gray-950">
|
||||||
<P2PDashboard />
|
<P2PDashboard />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChevronRight, Shield } from 'lucide-react';
|
import { ChevronRight, Shield, LogIn } from 'lucide-react';
|
||||||
import { usePezkuwi } from '../contexts/PezkuwiContext';
|
import { usePezkuwi } from '../contexts/PezkuwiContext';
|
||||||
import { useWallet } from '../contexts/WalletContext'; // Import useWallet
|
import { useWallet } from '../contexts/WalletContext';
|
||||||
import { formatBalance } from '@pezkuwi/lib/wallet';
|
import { formatBalance } from '@pezkuwi/lib/wallet';
|
||||||
import { getTrustScore } from '@pezkuwi/lib/scores';
|
import { getTrustScore } from '@pezkuwi/lib/scores';
|
||||||
|
import { getCurrentEra } from '@pezkuwi/lib/staking';
|
||||||
|
|
||||||
const HeroSection: React.FC = () => {
|
const HeroSection: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { api, isApiReady, peopleApi } = usePezkuwi();
|
const navigate = useNavigate();
|
||||||
const { selectedAccount } = useWallet(); // Use selectedAccount from WalletContext
|
const { api, isApiReady, assetHubApi, isAssetHubReady, peopleApi } = usePezkuwi();
|
||||||
|
const { selectedAccount } = useWallet();
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
activeProposals: 0,
|
activeProposals: 0,
|
||||||
totalVoters: 0,
|
totalVoters: 0,
|
||||||
@@ -17,90 +20,83 @@ const HeroSection: React.FC = () => {
|
|||||||
trustScore: null as number | null
|
trustScore: null as number | null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch governance stats from Relay Chain
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
const fetchGovernanceStats = async () => {
|
||||||
if (!api || !isApiReady) return;
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
let currentTrustScore: number | null = null; // null = not logged in
|
let activeProposals = 0;
|
||||||
if (selectedAccount?.address) {
|
try {
|
||||||
try {
|
const entries = await api.query.referenda.referendumInfoFor.entries();
|
||||||
// Use frontend fallback for trust score
|
activeProposals = entries.filter(([, info]) => {
|
||||||
if (peopleApi) {
|
const data = info.toJSON();
|
||||||
currentTrustScore = await getTrustScore(peopleApi, selectedAccount.address);
|
return data && typeof data === 'object' && 'ongoing' in data;
|
||||||
}
|
}).length;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (import.meta.env.DEV) console.warn('Failed to fetch trust score:', err);
|
if (import.meta.env.DEV) console.warn('Failed to fetch referenda:', err);
|
||||||
currentTrustScore = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let totalVoters = 0;
|
||||||
|
try {
|
||||||
|
const votingKeys = await api.query.convictionVoting.votingFor.keys();
|
||||||
|
const uniqueAccounts = new Set(votingKeys.map(key => key.args[0].toString()));
|
||||||
|
totalVoters = uniqueAccounts.size;
|
||||||
|
} catch (err) {
|
||||||
|
if (import.meta.env.DEV) console.warn('Failed to fetch voters:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStats(prev => ({ ...prev, activeProposals, totalVoters }));
|
||||||
|
};
|
||||||
|
fetchGovernanceStats();
|
||||||
|
}, [api, isApiReady]);
|
||||||
|
|
||||||
|
// Fetch staking stats from Asset Hub
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStakingStats = async () => {
|
||||||
|
if (!assetHubApi || !isAssetHubReady) return;
|
||||||
|
|
||||||
|
let tokensStaked = '0';
|
||||||
|
try {
|
||||||
|
const eraIndex = await getCurrentEra(assetHubApi);
|
||||||
|
if (eraIndex > 0) {
|
||||||
|
const totalStake = await assetHubApi.query.staking.erasTotalStake(eraIndex);
|
||||||
|
const formatted = formatBalance(totalStake.toString());
|
||||||
|
const [whole, frac] = formatted.split('.');
|
||||||
|
const formattedWhole = Number(whole).toLocaleString();
|
||||||
|
const formattedFrac = (frac || '00').slice(0, 2);
|
||||||
|
tokensStaked = `${formattedWhole}.${formattedFrac} HEZ`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (import.meta.env.DEV) console.warn('Failed to fetch total stake from AH:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStats(prev => ({ ...prev, tokensStaked }));
|
||||||
|
};
|
||||||
|
fetchStakingStats();
|
||||||
|
}, [assetHubApi, isAssetHubReady]);
|
||||||
|
|
||||||
|
// Fetch trust score from People Chain
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTrustScore = async () => {
|
||||||
|
if (!selectedAccount?.address) {
|
||||||
|
setStats(prev => ({ ...prev, trustScore: null }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!peopleApi) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch active (ongoing) referenda only
|
const score = await getTrustScore(peopleApi, selectedAccount.address);
|
||||||
let activeProposals = 0;
|
setStats(prev => ({ ...prev, trustScore: score }));
|
||||||
try {
|
} catch (err) {
|
||||||
const entries = await api.query.referenda.referendumInfoFor.entries();
|
if (import.meta.env.DEV) console.warn('Failed to fetch trust score:', err);
|
||||||
activeProposals = entries.filter(([, info]) => {
|
setStats(prev => ({ ...prev, trustScore: 0 }));
|
||||||
const data = info.toJSON();
|
|
||||||
return data && typeof data === 'object' && 'ongoing' in data;
|
|
||||||
}).length;
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) console.warn('Failed to fetch referenda:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch total staked tokens
|
|
||||||
let tokensStaked = '0';
|
|
||||||
try {
|
|
||||||
const currentEra = await api.query.staking.currentEra();
|
|
||||||
if (currentEra.isSome) {
|
|
||||||
const eraIndex = currentEra.unwrap().toNumber();
|
|
||||||
const totalStake = await api.query.staking.erasTotalStake(eraIndex);
|
|
||||||
const formatted = formatBalance(totalStake.toString());
|
|
||||||
const [whole, frac] = formatted.split('.');
|
|
||||||
const formattedWhole = Number(whole).toLocaleString();
|
|
||||||
const formattedFrac = (frac || '00').slice(0, 2);
|
|
||||||
tokensStaked = `${formattedWhole}.${formattedFrac} HEZ`;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) console.warn('Failed to fetch total stake:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count total voters from conviction voting
|
|
||||||
let totalVoters = 0;
|
|
||||||
try {
|
|
||||||
// Get all voting keys and count unique voters
|
|
||||||
const votingKeys = await api.query.convictionVoting.votingFor.keys();
|
|
||||||
// Each key represents a unique (account, track) pair
|
|
||||||
// Count unique accounts
|
|
||||||
const uniqueAccounts = new Set(votingKeys.map(key => key.args[0].toString()));
|
|
||||||
totalVoters = uniqueAccounts.size;
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) console.warn('Failed to fetch voters:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
setStats({
|
|
||||||
activeProposals,
|
|
||||||
totalVoters,
|
|
||||||
tokensStaked,
|
|
||||||
trustScore: currentTrustScore
|
|
||||||
});
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('✅ Hero stats updated:', {
|
|
||||||
activeProposals,
|
|
||||||
totalVoters,
|
|
||||||
tokensStaked,
|
|
||||||
trustScore: currentTrustScore
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (import.meta.env.DEV) console.error('Failed to fetch hero stats:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
fetchTrustScore();
|
||||||
fetchStats();
|
}, [peopleApi, selectedAccount]);
|
||||||
}, [api, isApiReady, peopleApi, selectedAccount]); // Add peopleApi to dependencies
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen flex items-center justify-start overflow-hidden bg-gray-950">
|
<section className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gray-950">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<img
|
<img
|
||||||
@@ -143,7 +139,17 @@ const HeroSection: React.FC = () => {
|
|||||||
<div className="text-xs sm:text-sm text-gray-300 font-medium">{t('hero.stats.tokensStaked', 'Tokens Staked')}</div>
|
<div className="text-xs sm:text-sm text-gray-300 font-medium">{t('hero.stats.tokensStaked', 'Tokens Staked')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-green-500/40 p-4 sm:p-6 hover:border-green-500/60 transition-all">
|
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-green-500/40 p-4 sm:p-6 hover:border-green-500/60 transition-all">
|
||||||
<div className="text-base sm:text-2xl font-bold text-green-400 mb-2">{stats.trustScore !== null ? stats.trustScore : t('hero.stats.loginToSee', 'Login')}</div>
|
{stats.trustScore !== null ? (
|
||||||
|
<div className="text-base sm:text-2xl font-bold text-green-400 mb-2">{stats.trustScore}</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
className="text-xs sm:text-sm font-medium text-green-400 hover:text-green-300 transition-colors flex items-center gap-1 mx-auto mb-2"
|
||||||
|
>
|
||||||
|
<LogIn className="w-3.5 h-3.5" />
|
||||||
|
{t('hero.stats.loginToSee', 'Görmek için giriş yapın')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="text-xs sm:text-sm text-gray-300 font-medium">{t('hero.stats.trustScore', 'Trust Score')}</div>
|
<div className="text-xs sm:text-sm text-gray-300 font-medium">{t('hero.stats.trustScore', 'Trust Score')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+24
-3
@@ -32,12 +32,33 @@ export default defineConfig(() => ({
|
|||||||
react(),
|
react(),
|
||||||
nodePolyfills({
|
nodePolyfills({
|
||||||
globals: {
|
globals: {
|
||||||
Buffer: true,
|
Buffer: false,
|
||||||
global: true,
|
global: false,
|
||||||
process: true,
|
process: false,
|
||||||
},
|
},
|
||||||
protocolImports: true,
|
protocolImports: true,
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
name: 'node-globals-shim',
|
||||||
|
transformIndexHtml() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
children: `
|
||||||
|
window.global = window.global || window;
|
||||||
|
window.process = window.process || { env: {}, browser: true, version: "" };
|
||||||
|
`,
|
||||||
|
injectTo: 'head-prepend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: { type: 'module' },
|
||||||
|
children: `import { Buffer } from 'buffer'; window.Buffer = window.Buffer || Buffer;`,
|
||||||
|
injectTo: 'head-prepend',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
mainFields: ['module', 'main', 'exports'],
|
mainFields: ['module', 'main', 'exports'],
|
||||||
|
|||||||
Reference in New Issue
Block a user