feat: add wallet connection options and mobile navigation

- New WalletConnectModal with Extension/WalletConnect options
- Mobile detection: shows only Pezkuwi Wallet on mobile
- Deep link support for pezkuwiwallet:// scheme
- Hamburger menu for mobile navigation
- Full mobile menu with all navigation items
This commit is contained in:
2026-02-11 00:41:45 +03:00
parent 6f6cedf239
commit 11241e8252
3 changed files with 577 additions and 5 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 145 KiB

+172 -5
View File
@@ -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 } 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 } from 'lucide-react';
import GovernanceInterface from './GovernanceInterface';
import RewardDistribution from './RewardDistribution';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -27,7 +27,7 @@ import { useWebSocket } from '@/contexts/WebSocketContext';
import { StakingDashboard } from './staking/StakingDashboard';
import { MultiSigWallet } from './wallet/MultiSigWallet';
import { useWallet } from '@/contexts/WalletContext';
import { PezkuwiWalletButton } from './PezkuwiWalletButton';
import { WalletConnectModal } from './WalletConnectModal';
import { DEXDashboard } from './dex/DEXDashboard';
import { P2PDashboard } from './p2p/P2PDashboard';
import EducationPlatform from '../pages/EducationPlatform';
@@ -47,6 +47,7 @@ const AppLayout: React.FC = () => {
const [showDEX, setShowDEX] = useState(false);
const [showEducation, setShowEducation] = useState(false);
const [showP2P, setShowP2P] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { t } = useTranslation();
const { isConnected } = useWebSocket();
useWallet();
@@ -69,8 +70,19 @@ const AppLayout: React.FC = () => {
PezkuwiChain
</span>
</div>
{/* CENTER & RIGHT: Menu + Actions in same row */}
{/* Mobile: Hamburger + Wallet */}
<div className="flex lg:hidden items-center gap-2">
<WalletConnectModal />
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors"
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
{/* CENTER & RIGHT: Menu + Actions in same row (Desktop only) */}
<div className="hidden lg:flex items-center space-x-4 flex-1 justify-start ml-8 pr-4">
{user ? (
<>
@@ -270,12 +282,167 @@ const AppLayout: React.FC = () => {
<NotificationBell />
<LanguageSwitcher />
<PezkuwiWalletButton />
<WalletConnectModal />
</div>
</div>
</div>
</nav>
{/* Mobile Menu Panel */}
{mobileMenuOpen && (
<div className="fixed inset-0 z-30 lg:hidden">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Menu Panel */}
<div className="fixed top-16 right-0 bottom-0 w-72 bg-gray-900 border-l border-gray-800 overflow-y-auto">
<div className="p-4 space-y-2">
{user ? (
<>
<button
onClick={() => { navigate('/dashboard'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<LayoutDashboard className="w-5 h-5" />
Dashboard
</button>
<button
onClick={() => { navigate('/wallet'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<Wallet className="w-5 h-5" />
Wallet
</button>
<button
onClick={() => { navigate('/citizens'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-cyan-400 hover:bg-gray-800 flex items-center gap-3"
>
<Users className="w-5 h-5" />
Citizens Portal
</button>
<button
onClick={() => { navigate('/be-citizen'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-cyan-300 hover:bg-gray-800 flex items-center gap-3"
>
<Users className="w-5 h-5" />
Be Citizen
</button>
<div className="border-t border-gray-800 my-2" />
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">Governance</div>
<button
onClick={() => { setShowProposalWizard(true); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<FileEdit className="w-5 h-5" />
Proposals
</button>
<button
onClick={() => { setShowDelegation(true); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<Users2 className="w-5 h-5" />
Delegation
</button>
<button
onClick={() => { setShowTreasury(true); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<PiggyBank className="w-5 h-5" />
Treasury
</button>
<div className="border-t border-gray-800 my-2" />
<div className="px-4 py-2 text-xs text-gray-500 uppercase tracking-wider">Trading</div>
<button
onClick={() => { setShowDEX(true); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<Droplet className="w-5 h-5" />
DEX Pools
</button>
<button
onClick={() => { navigate('/p2p'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<ArrowRightLeft className="w-5 h-5" />
P2P
</button>
<button
onClick={() => { navigate('/presale'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<Coins className="w-5 h-5" />
Presale
</button>
<button
onClick={() => { setShowStaking(true); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<TrendingUp className="w-5 h-5" />
Staking
</button>
<div className="border-t border-gray-800 my-2" />
<button
onClick={() => { navigate('/profile/settings'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<Settings className="w-5 h-5" />
Settings
</button>
<button
onClick={async () => { await signOut(); navigate('/login'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-red-400 hover:bg-gray-800 flex items-center gap-3"
>
<LogIn className="w-5 h-5 rotate-180" />
Logout
</button>
</>
) : (
<>
<button
onClick={() => { navigate('/be-citizen'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg text-cyan-300 hover:bg-gray-800 flex items-center gap-3"
>
<Users className="w-5 h-5" />
Be Citizen
</button>
<button
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
className="w-full text-left px-4 py-3 rounded-lg bg-green-600 hover:bg-green-700 text-white flex items-center gap-3"
>
<LogIn className="w-5 h-5" />
Login
</button>
</>
)}
<div className="border-t border-gray-800 my-2" />
<a
href="/docs"
onClick={() => setMobileMenuOpen(false)}
className="w-full text-left px-4 py-3 rounded-lg text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-3"
>
<ExternalLink className="w-5 h-5" />
Docs
</a>
</div>
</div>
</div>
)}
{/* Main Content */}
<main>
{/* Conditional Rendering for Features */}
+405
View File
@@ -0,0 +1,405 @@
import React, { useState, useEffect } from 'react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Wallet, Check, Copy, LogOut, Smartphone, Monitor, ExternalLink, QrCode } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
// Detect if user is on mobile
const isMobile = (): boolean => {
if (typeof window === 'undefined') return false;
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
};
// Pezkuwi Wallet deep link
const PEZKUWI_WALLET_DEEP_LINK = 'pezkuwiwallet://';
const PLAY_STORE_LINK = 'https://play.google.com/store/apps/details?id=io.novafoundation.nova.market';
const APP_STORE_LINK = 'https://apps.apple.com/app/nova-polkadot-kusama-wallet/id1597119355';
type ConnectionMethod = 'select' | 'extension' | 'walletconnect';
export const WalletConnectModal: React.FC = () => {
const {
accounts,
selectedAccount,
setSelectedAccount,
connectWallet,
disconnectWallet,
error
} = usePezkuwi();
const [isOpen, setIsOpen] = useState(false);
const [connectionMethod, setConnectionMethod] = useState<ConnectionMethod>('select');
const [showAccountSelect, setShowAccountSelect] = useState(false);
const { toast } = useToast();
const mobile = isMobile();
// Reset to selection when modal opens
useEffect(() => {
if (isOpen && !selectedAccount) {
setConnectionMethod('select');
}
}, [isOpen, selectedAccount]);
const handleConnect = () => {
setIsOpen(true);
setConnectionMethod('select');
};
const handleExtensionConnect = async () => {
setConnectionMethod('extension');
await connectWallet();
if (accounts.length > 0) {
setShowAccountSelect(true);
}
};
const handleWalletConnectConnect = () => {
setConnectionMethod('walletconnect');
if (mobile) {
// Try to open Pezkuwi Wallet app
// First try deep link, then fallback to store
const deepLink = PEZKUWI_WALLET_DEEP_LINK;
// Create a hidden iframe to try the deep link
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = deepLink;
document.body.appendChild(iframe);
// Fallback to app store after timeout
setTimeout(() => {
document.body.removeChild(iframe);
// Check if app was opened by checking if page is still visible
if (!document.hidden) {
// App didn't open, redirect to store
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
window.location.href = isIOS ? APP_STORE_LINK : PLAY_STORE_LINK;
}
}, 2500);
toast({
title: "Pezkuwi Wallet açılıyor...",
description: "Uygulama yüklü değilse mağazaya yönlendirileceksiniz",
});
} else {
// Desktop - show QR code or instructions
toast({
title: "WalletConnect",
description: "QR kod desteği yakında eklenecek. Şimdilik browser extension kullanın.",
});
}
};
const handleSelectAccount = (account: typeof accounts[0]) => {
setSelectedAccount(account);
setIsOpen(false);
setShowAccountSelect(false);
toast({
title: "Hesap Bağlandı",
description: `${account.meta.name} - ${formatAddress(account.address)}`,
});
};
const handleDisconnect = () => {
disconnectWallet();
setIsOpen(false);
toast({
title: "Cüzdan Bağlantısı Kesildi",
description: "Cüzdanınızın bağlantısı kesildi",
});
};
const formatAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
const copyAddress = () => {
if (selectedAccount) {
navigator.clipboard.writeText(selectedAccount.address);
toast({
title: "Adres Kopyalandı",
description: "Adres panoya kopyalandı",
});
}
};
// Connected state - show account info
if (selectedAccount) {
return (
<div className="flex items-center gap-2">
<Button
variant="outline"
className="bg-green-500/20 border-green-500/50 text-green-400 hover:bg-green-500/30"
onClick={() => setIsOpen(true)}
>
<Wallet className="w-4 h-4 mr-2" />
{selectedAccount.meta.name || 'Account'}
<Badge className="ml-2 bg-green-500/30 text-green-300 border-0">
{formatAddress(selectedAccount.address)}
</Badge>
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleDisconnect}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
<LogOut className="w-4 h-4" />
</Button>
{/* Account Details Dialog */}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="bg-gray-900 border-gray-800">
<DialogHeader>
<DialogTitle className="text-white">Hesap Detayları</DialogTitle>
<DialogDescription className="text-gray-400">
Bağlı Pezkuwi hesabınız
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Hesap Adı</div>
<div className="text-white font-medium">
{selectedAccount.meta.name || 'İsimsiz Hesap'}
</div>
</div>
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Adres</div>
<div className="flex items-center justify-between">
<code className="text-white text-sm font-mono break-all">
{selectedAccount.address}
</code>
<Button
variant="ghost"
size="icon"
onClick={copyAddress}
className="text-gray-400 hover:text-white flex-shrink-0 ml-2"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Kaynak</div>
<div className="text-white">
{selectedAccount.meta.source || 'pezkuwi'}
</div>
</div>
{accounts.length > 1 && (
<div>
<div className="text-sm text-gray-400 mb-2">Hesap Değiştir</div>
<div className="space-y-2 max-h-48 overflow-y-auto">
{accounts.map((account) => (
<button
key={account.address}
onClick={() => handleSelectAccount(account)}
className={`w-full p-3 rounded-lg border transition-all flex items-center justify-between ${
account.address === selectedAccount.address
? 'bg-green-500/20 border-green-500/50'
: 'bg-gray-800/50 border-gray-700 hover:border-gray-600'
}`}
>
<div className="text-left">
<div className="text-white font-medium">
{account.meta.name || 'İsimsiz'}
</div>
<div className="text-gray-400 text-xs font-mono">
{formatAddress(account.address)}
</div>
</div>
{account.address === selectedAccount.address && (
<Check className="w-5 h-5 text-green-400" />
)}
</button>
))}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
}
// Not connected - show connect button
return (
<>
<Button
onClick={handleConnect}
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 text-white"
>
<Wallet className="w-4 h-4 mr-2" />
Cüzdan Bağla
</Button>
{/* Connection Method Selection Dialog */}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="bg-gray-900 border-gray-800 sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-white text-center text-xl">
Cüzdan Bağla
</DialogTitle>
<DialogDescription className="text-gray-400 text-center">
Bağlantı yönteminizi seçin
</DialogDescription>
</DialogHeader>
{/* Connection Method Selection */}
{connectionMethod === 'select' && (
<div className="space-y-3 py-4">
{/* Browser Extension Option - Hide on mobile */}
{!mobile && (
<button
onClick={handleExtensionConnect}
className="w-full p-4 rounded-xl border-2 border-gray-700 bg-gray-800/50 hover:border-green-500/50 hover:bg-gray-800 transition-all text-left flex items-center gap-4"
>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-green-500 to-green-700 flex items-center justify-center flex-shrink-0">
<Monitor className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="text-white font-semibold mb-1">Browser Extension</div>
<div className="text-gray-400 text-sm">
Polkadot.js veya Pezkuwi Extension ile bağlan
</div>
</div>
</button>
)}
{/* Pezkuwi Wallet Option */}
<button
onClick={handleWalletConnectConnect}
className="w-full p-4 rounded-xl border-2 border-gray-700 bg-gray-800/50 hover:border-yellow-500/50 hover:bg-gray-800 transition-all text-left flex items-center gap-4"
>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-yellow-500 to-orange-600 flex items-center justify-center flex-shrink-0">
<Smartphone className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="text-white font-semibold mb-1">Pezkuwi Wallet</div>
<div className="text-gray-400 text-sm">
{mobile
? 'Pezkuwi Wallet uygulamasını aç'
: 'QR kod ile mobil cüzdanla bağlan'}
</div>
</div>
{!mobile && (
<QrCode className="w-5 h-5 text-gray-500" />
)}
</button>
{/* Download Wallet Link */}
<div className="pt-2 text-center">
<a
href={mobile ? ((/iPhone|iPad|iPod/i.test(navigator.userAgent)) ? APP_STORE_LINK : PLAY_STORE_LINK) : PLAY_STORE_LINK}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-gray-400 inline-flex items-center gap-1"
>
Pezkuwi Wallet&apos;ı indir
<ExternalLink className="w-3 h-3" />
</a>
</div>
</div>
)}
{/* Extension Error State */}
{connectionMethod === 'extension' && error && error.includes('not found') && (
<div className="space-y-4 py-4">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-500/20 flex items-center justify-center">
<Monitor className="w-8 h-8 text-red-400" />
</div>
<h3 className="text-white font-semibold mb-2">Extension Bulunamadı</h3>
<p className="text-gray-400 text-sm mb-4">
Pezkuwi Wallet veya Polkadot.js extension yüklü değil
</p>
</div>
<a
href="https://chrome.google.com/webstore/detail/polkadot-js-extension/mopnmbcafieddcagagdcbnhejhlodfdd"
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Button className="w-full bg-green-600 hover:bg-green-700">
<ExternalLink className="w-4 h-4 mr-2" />
Chrome Web Store&apos;dan Yükle
</Button>
</a>
<Button
variant="outline"
className="w-full border-gray-700"
onClick={() => setConnectionMethod('select')}
>
Geri Dön
</Button>
</div>
)}
{/* WalletConnect State - Desktop QR */}
{connectionMethod === 'walletconnect' && !mobile && (
<div className="space-y-4 py-4">
<div className="text-center">
<div className="w-48 h-48 mx-auto mb-4 rounded-xl bg-white p-4 flex items-center justify-center">
<div className="text-gray-400 text-center">
<QrCode className="w-16 h-16 mx-auto mb-2 text-gray-300" />
<p className="text-sm">QR Kod</p>
<p className="text-xs text-gray-500">Yakında</p>
</div>
</div>
<p className="text-gray-400 text-sm mb-4">
Pezkuwi Wallet uygulamasıyla QR kodu tarayın
</p>
</div>
<Button
variant="outline"
className="w-full border-gray-700"
onClick={() => setConnectionMethod('select')}
>
Geri Dön
</Button>
</div>
)}
{/* Account Selection */}
{showAccountSelect && accounts.length > 0 && (
<div className="space-y-4 py-4">
<div className="text-sm text-gray-400 mb-2">Hesap Seçin</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{accounts.map((account) => (
<button
key={account.address}
onClick={() => handleSelectAccount(account)}
className="w-full p-4 rounded-lg border border-gray-700 bg-gray-800/50 hover:border-green-500/50 hover:bg-gray-800 transition-all text-left"
>
<div className="text-white font-medium mb-1">
{account.meta.name || 'İsimsiz Hesap'}
</div>
<div className="text-gray-400 text-sm font-mono">
{formatAddress(account.address)}
</div>
</button>
))}
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
};