Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/app/components/nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@ import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { Icon, ICON_IDS } from "@/components/icons";
import { useProgressBar } from "./TopLoadingBar";
import { useWalletState } from "../hooks/useWalletState";

const Nav = memo(() => {
const hasAnomaly = true;
const router = useRouter();
const pathname = usePathname();
const { start, done } = useProgressBar();
const { wallet, isChecking, refreshWalletState } = useWalletState();

const walletLabel = wallet?.connected
? wallet.publicKey
? `${wallet.publicKey.slice(0, 4)}...${wallet.publicKey.slice(-4)}`
: "Wallet connected"
: "Connect Wallet";

const handleConnectWallet = useCallback(async () => {
start();
await new Promise((resolve) => setTimeout(resolve, 1200));
const state = await refreshWalletState();
done();
alert("Connect Wallet clicked! (Add your Web3 logic here)");
}, [start, done]);

if (state?.connected) {
alert(`Connected wallet: ${state.publicKey ?? "unknown"}`);
} else {
alert("No active Stellar wallet detected. Please connect your extension.");
}
}, [refreshWalletState, start, done]);

return (
<main className="sticky top-0 z-50 bg-zinc-950 border-b border-zinc-800">
Expand Down Expand Up @@ -48,8 +61,11 @@ const Nav = memo(() => {
<div className="flex flex-wrap items-center gap-2">
<button
onClick={handleConnectWallet}
disabled={isChecking}
className="wallet-btn group flex min-w-0 items-center gap-2 px-3 sm:gap-2.5 sm:px-4 py-2 rounded-2xl font-semibold text-sm sm:text-base transition-all duration-300 hover:shadow-xl active:scale-95 whitespace-nowrap"
>
<Wallet className="w-5 h-5 transition-transform group-hover:rotate-12" />
<span className="truncate">{walletLabel}</span>
<Icon id={ICON_IDS.wallet} size={18} className="transition-transform group-hover:rotate-12" />
<span className="truncate">
Connect <span className="hidden md:inline">Wallet</span>
Expand Down
144 changes: 144 additions & 0 deletions src/app/components/providers/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client';

import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';

export interface WalletState {
publicKey: string | null;
connected: boolean;
source: 'extension' | 'fallback' | 'none';
lastCheckedAt: number;
}

interface WalletContextType {
wallet: WalletState | null;
isChecking: boolean;
error: string | null;
refreshWalletState: () => Promise<WalletState | null>;
}

const WalletContext = createContext<WalletContextType | null>(null);

const CACHE_TTL = 2500;
let cache: { expiresAt: number; value: WalletState | null } | null = null;
let pendingRequest: Promise<WalletState | null> | null = null;

const createFallbackState = (source: WalletState['source']): WalletState => ({
publicKey: null,
connected: false,
source,
lastCheckedAt: Date.now(),
});

async function queryExtensionWalletState(): Promise<WalletState> {
if (typeof window === 'undefined') {
return createFallbackState('none');
}

const anyWindow = window as any;
const extension =
anyWindow.stellar || anyWindow.Freighter || anyWindow.freighterApi || anyWindow.Horizon || null;

try {
if (typeof extension?.getPublicKey === 'function') {
const publicKey = await extension.getPublicKey();
return {
publicKey: typeof publicKey === 'string' ? publicKey : null,
connected: Boolean(publicKey),
source: 'extension',
lastCheckedAt: Date.now(),
};
}

if (typeof extension?.publicKey === 'string') {
return {
publicKey: extension.publicKey,
connected: true,
source: 'extension',
lastCheckedAt: Date.now(),
};
}

if (typeof extension?.isConnected === 'function') {
const connected = await extension.isConnected();
return {
publicKey: null,
connected: Boolean(connected),
source: 'extension',
lastCheckedAt: Date.now(),
};
}
} catch {
// Extension query failure should not break the app. Fall back to cached state.
}

return createFallbackState('none');
}

async function getWalletState(): Promise<WalletState | null> {
const now = Date.now();
if (cache && cache.expiresAt > now) {
return cache.value;
}

if (pendingRequest) {
return pendingRequest;
}

pendingRequest = queryExtensionWalletState().then((state) => {
cache = {
expiresAt: Date.now() + CACHE_TTL,
value: state,
};
pendingRequest = null;
return state;
});

return pendingRequest;
}

export function WalletProvider({ children }: { children: React.ReactNode }) {
const [wallet, setWallet] = useState<WalletState | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);

const refreshWalletState = React.useCallback(async () => {
setIsChecking(true);
setError(null);

try {
const state = await getWalletState();
setWallet(state);
return state;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to refresh wallet state');
return null;
} finally {
setIsChecking(false);
}
}, []);

useEffect(() => {
refreshWalletState();
}, [refreshWalletState]);

const value = useMemo(
() => ({ wallet, isChecking, error, refreshWalletState }),
[wallet, isChecking, error, refreshWalletState],
);

return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
}

export function useWalletState() {
const context = useContext(WalletContext);
if (!context) {
throw new Error('useWalletState must be used within a WalletProvider');
}
return context;
}
19 changes: 19 additions & 0 deletions src/app/governance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'lucide-react';
import { withShortenedAddressField } from '@/utils/addressUtils';
import { useRAFInterval } from '@/app/hooks/useRAFInterval';
import { useWalletState } from '@/app/hooks/useWalletState';

// --- Types ---
interface Proposal {
Expand All @@ -38,6 +39,13 @@ const MOCK_PROPOSALS: Proposal[] = [

export default function GovernancePage() {
const [activeTab, setActiveTab] = useState<'all' | 'active' | 'archived'>('all');
const { wallet, isChecking, refreshWalletState } = useWalletState();

const walletStatus = wallet?.connected
? wallet.publicKey
? `${wallet.publicKey.slice(0, 4)}...${wallet.publicKey.slice(-4)}`
: 'Connected'
: 'No wallet connected';

// Pre-compute shortened addresses on data ingestion to avoid render-time string slicing
const transformedProposals = useMemo(
Expand Down Expand Up @@ -69,6 +77,14 @@ export default function GovernancePage() {
<p className="text-sm text-gray-500 mb-1">Admin / Consensus</p>
<h1 className="text-3xl font-bold tracking-tight">Governance & Proposals</h1>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={() => refreshWalletState()}
disabled={isChecking}
className="flex items-center gap-2 bg-[#161b22] border border-gray-800 hover:bg-gray-800 text-gray-300 px-4 py-2 rounded-lg transition-all text-sm font-medium"
>
<Wallet size={16} className="text-purple-400" />
{wallet?.connected ? walletStatus : 'Connect Freighter Wallet'}
<div className="flex gap-3">
<button className="flex items-center gap-2 bg-[#161b22] border border-gray-800 hover:bg-gray-800 text-gray-300 px-4 py-2 rounded-lg transition-all text-sm font-medium">
<Icon id={ICON_IDS.wallet} size={16} className="text-purple-400" />
Expand All @@ -81,6 +97,9 @@ export default function GovernancePage() {
</div>
</div>

<div className="mb-3 text-sm text-gray-400">
Active wallet status: <span className="text-white">{walletStatus}</span>
</div>
{/* --- Consensus Statistics Rows --- */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<StatCard title="Total Staking Power" value="2.85M SF" icon={<Icon id={ICON_IDS.vote} size={20} className="text-blue-400" />} subtitle="Active voting weights" />
Expand Down
1 change: 1 addition & 0 deletions src/app/hooks/useWalletState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useWalletState, WalletProvider, type WalletState } from "@/app/components/providers/WalletProvider";
16 changes: 11 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./globals.css";
import { ThemeProvider } from "./components/ThemeProvider";
import { ProgressBarProvider } from "./components/TopLoadingBar";
import { UserProvider } from "./components/providers/UserProvider";
import { WalletProvider } from "./components/providers/WalletProvider";
import { QueryProvider } from "./components/providers/QueryProvider";
import Script from "next/script";
import { SocketProvider } from "./components/providers/SocketProvider";
Expand Down Expand Up @@ -89,11 +90,16 @@ export default function RootLayout({
disableTransitionOnChange
>
<UserProvider>
<SocketProvider>
<QueryProvider>
<ProgressBarProvider>{children}</ProgressBarProvider>
</QueryProvider>
</SocketProvider>
<WalletProvider>
<ProgressBarProvider>
{children}
</ProgressBarProvider>
</WalletProvider>
<QueryProvider>
<ProgressBarProvider>
{children}
</ProgressBarProvider>
</QueryProvider>
</UserProvider>
</ThemeProvider>
</body>
Expand Down
Loading