diff --git a/screenshot.png b/screenshot.png index 71e0e0d9..7cfc4739 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/web/src/App.css b/web/src/App.css index e73a2016..174c0093 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -1,8 +1,6 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + width: 100%; + min-height: 100vh; } .logo { @@ -50,4 +48,4 @@ .read-the-docs { color: #5f7676; -} \ No newline at end of file +} diff --git a/web/src/components/AppLayout.tsx b/web/src/components/AppLayout.tsx index baa06224..63d4dfc8 100644 --- a/web/src/components/AppLayout.tsx +++ b/web/src/components/AppLayout.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAuth } from '@/contexts/AuthContext'; @@ -19,7 +19,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview'; import { FundingProposal } from './treasury/FundingProposal'; import { SpendingHistory } from './treasury/SpendingHistory'; 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 RewardDistribution from './RewardDistribution'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -48,11 +48,26 @@ const AppLayout: React.FC = () => { const [showEducation, setShowEducation] = useState(false); const [showP2P, setShowP2P] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [openMenu, setOpenMenu] = useState(null); + const gridRef = useRef(null); const { t } = useTranslation(); const { isConnected } = useWebSocket(); useWallet(); 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 // Supabase admin_roles is optional (table may not exist) React.useEffect(() => { @@ -62,7 +77,7 @@ const AppLayout: React.FC = () => {
{/* Navigation */} + {/* Button Grid (logged in only) */} + {user && ( +
+
+ {/* Dashboard */} + + {/* Wallet */} + + {/* Be Citizen */} + + {/* Governance (dropdown) */} +
+ + {openMenu === 'governance' && ( +
+ + + + + +
+ )} +
+ {/* Trading (dropdown) */} +
+ + {openMenu === 'trading' && ( +
+ + + + + +
+ )} +
+ {/* Education */} + + {/* Settings */} + + {/* Logout */} + +
+
+ )} + {/* Mobile Menu Panel */} {mobileMenuOpen && (
@@ -432,13 +408,13 @@ const AppLayout: React.FC = () => {
{/* Conditional Rendering for Features */} {showDEX ? ( -
+
) : showProposalWizard ? ( -
+
{ @@ -450,25 +426,25 @@ const AppLayout: React.FC = () => {
) : showDelegation ? ( -
+
) : showForum ? ( -
+
) : showModeration ? ( -
+
) : showTreasury ? ( -
+

@@ -518,7 +494,7 @@ const AppLayout: React.FC = () => {

) : showStaking ? ( -
+

@@ -532,7 +508,7 @@ const AppLayout: React.FC = () => {

) : showMultiSig ? ( -
+

@@ -546,11 +522,11 @@ const AppLayout: React.FC = () => {

) : showEducation ? ( -
+
) : showP2P ? ( -
+
) : ( diff --git a/web/src/components/HeroSection.tsx b/web/src/components/HeroSection.tsx index 04e8ea8c..76423157 100644 --- a/web/src/components/HeroSection.tsx +++ b/web/src/components/HeroSection.tsx @@ -1,15 +1,18 @@ import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { ChevronRight, Shield } from 'lucide-react'; +import { ChevronRight, Shield, LogIn } from 'lucide-react'; import { usePezkuwi } from '../contexts/PezkuwiContext'; -import { useWallet } from '../contexts/WalletContext'; // Import useWallet +import { useWallet } from '../contexts/WalletContext'; import { formatBalance } from '@pezkuwi/lib/wallet'; import { getTrustScore } from '@pezkuwi/lib/scores'; +import { getCurrentEra } from '@pezkuwi/lib/staking'; const HeroSection: React.FC = () => { const { t } = useTranslation(); - const { api, isApiReady, peopleApi } = usePezkuwi(); - const { selectedAccount } = useWallet(); // Use selectedAccount from WalletContext + const navigate = useNavigate(); + const { api, isApiReady, assetHubApi, isAssetHubReady, peopleApi } = usePezkuwi(); + const { selectedAccount } = useWallet(); const [stats, setStats] = useState({ activeProposals: 0, totalVoters: 0, @@ -17,90 +20,83 @@ const HeroSection: React.FC = () => { trustScore: null as number | null }); + // Fetch governance stats from Relay Chain useEffect(() => { - const fetchStats = async () => { + const fetchGovernanceStats = async () => { if (!api || !isApiReady) return; - let currentTrustScore: number | null = null; // null = not logged in - if (selectedAccount?.address) { - try { - // Use frontend fallback for trust score - if (peopleApi) { - currentTrustScore = await getTrustScore(peopleApi, selectedAccount.address); - } - } catch (err) { - if (import.meta.env.DEV) console.warn('Failed to fetch trust score:', err); - currentTrustScore = 0; - } + let activeProposals = 0; + try { + const entries = await api.query.referenda.referendumInfoFor.entries(); + activeProposals = entries.filter(([, info]) => { + 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); } + 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 { - // Fetch active (ongoing) referenda only - let activeProposals = 0; - try { - const entries = await api.query.referenda.referendumInfoFor.entries(); - activeProposals = entries.filter(([, info]) => { - 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); + const score = await getTrustScore(peopleApi, selectedAccount.address); + setStats(prev => ({ ...prev, trustScore: score })); + } catch (err) { + if (import.meta.env.DEV) console.warn('Failed to fetch trust score:', err); + setStats(prev => ({ ...prev, trustScore: 0 })); } }; - - fetchStats(); - }, [api, isApiReady, peopleApi, selectedAccount]); // Add peopleApi to dependencies + fetchTrustScore(); + }, [peopleApi, selectedAccount]); return ( -
+
{/* Background Image */}
{
{t('hero.stats.tokensStaked', 'Tokens Staked')}
-
{stats.trustScore !== null ? stats.trustScore : t('hero.stats.loginToSee', 'Login')}
+ {stats.trustScore !== null ? ( +
{stats.trustScore}
+ ) : ( + + )}
{t('hero.stats.trustScore', 'Trust Score')}
diff --git a/web/vite.config.ts b/web/vite.config.ts index a81876e5..78c0e60a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -32,12 +32,33 @@ export default defineConfig(() => ({ react(), nodePolyfills({ globals: { - Buffer: true, - global: true, - process: true, + Buffer: false, + global: false, + process: false, }, 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), resolve: { mainFields: ['module', 'main', 'exports'],