mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-28 15:27:57 +00:00
Initial commit - PezkuwiChain Web Governance App
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Shield, Users, Key, CheckCircle, XCircle, Clock, Send } from 'lucide-react';
|
||||
|
||||
interface MultiSigTransaction {
|
||||
id: string;
|
||||
to: string;
|
||||
amount: number;
|
||||
token: string;
|
||||
description: string;
|
||||
requiredSignatures: number;
|
||||
currentSignatures: number;
|
||||
status: 'pending' | 'executed' | 'rejected';
|
||||
signers: string[];
|
||||
}
|
||||
|
||||
export const MultiSigWallet: React.FC = () => {
|
||||
const [amount, setAmount] = useState('');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const transactions: MultiSigTransaction[] = [
|
||||
{
|
||||
id: '1',
|
||||
to: '0x742d...29Bb',
|
||||
amount: 5000,
|
||||
token: 'HEZ',
|
||||
description: 'Development fund payment',
|
||||
requiredSignatures: 3,
|
||||
currentSignatures: 2,
|
||||
status: 'pending',
|
||||
signers: ['Alice', 'Bob']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
to: '0x891a...45Cc',
|
||||
amount: 10000,
|
||||
token: 'PEZ',
|
||||
description: 'Marketing campaign',
|
||||
requiredSignatures: 3,
|
||||
currentSignatures: 3,
|
||||
status: 'executed',
|
||||
signers: ['Alice', 'Bob', 'Charlie']
|
||||
}
|
||||
];
|
||||
|
||||
const handleCreateTransaction = () => {
|
||||
console.log('Creating multi-sig transaction:', { amount, recipient, description });
|
||||
};
|
||||
|
||||
const handleSign = (txId: string) => {
|
||||
console.log('Signing transaction:', txId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Wallet Balance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">50,000 HEZ</div>
|
||||
<p className="text-xs text-gray-500 mt-1">25,000 PEZ</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Required Signatures</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">3 of 5</div>
|
||||
<p className="text-xs text-gray-500 mt-1">Signers required</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-gray-400">Pending Transactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-500">2</div>
|
||||
<p className="text-xs text-gray-500 mt-1">Awaiting signatures</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Create Transaction</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Recipient Address</Label>
|
||||
<Input
|
||||
placeholder="0x..."
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Amount</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
placeholder="Transaction purpose"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleCreateTransaction} className="bg-green-600 hover:bg-green-700">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Create Transaction
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle>Pending Transactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{transactions.map((tx) => (
|
||||
<Card key={tx.id} className="bg-gray-800 border-gray-700">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-white">{tx.description}</span>
|
||||
<Badge variant={tx.status === 'executed' ? 'default' : tx.status === 'pending' ? 'secondary' : 'destructive'}>
|
||||
{tx.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
To: {tx.to} | Amount: {tx.amount} {tx.token}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Progress value={(tx.currentSignatures / tx.requiredSignatures) * 100} className="w-32" />
|
||||
<span className="text-sm text-gray-400">
|
||||
{tx.currentSignatures}/{tx.requiredSignatures} signatures
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{tx.status === 'pending' && (
|
||||
<Button size="sm" onClick={() => handleSign(tx.id)} className="bg-blue-600 hover:bg-blue-700">
|
||||
<Key className="w-4 h-4 mr-1" />
|
||||
Sign
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Send, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface TransactionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
type: 'send' | 'vote' | 'delegate';
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export const TransactionModal: React.FC<TransactionModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
type,
|
||||
data
|
||||
}) => {
|
||||
const { address, signTransaction, signMessage } = useWallet();
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [txHash, setTxHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSendTransaction = async () => {
|
||||
if (!recipient || !amount) {
|
||||
setError('Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const tx = {
|
||||
to: recipient,
|
||||
value: '0x' + (parseFloat(amount) * 1e18).toString(16),
|
||||
data: '0x',
|
||||
};
|
||||
|
||||
const hash = await signTransaction(tx);
|
||||
setTxHash(hash);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Transaction failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignMessage = async () => {
|
||||
if (!message) {
|
||||
setError('Please enter a message to sign');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const signature = await signMessage(message);
|
||||
setTxHash(signature);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to sign message');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setRecipient('');
|
||||
setAmount('');
|
||||
setMessage('');
|
||||
setTxHash(null);
|
||||
setError(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={resetForm}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Send className="h-5 w-5 text-kesk" />
|
||||
{type === 'send' ? 'Send PZK' : type === 'vote' ? 'Cast Vote' : 'Delegate Voting Power'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{type === 'send'
|
||||
? 'Send PZK tokens to another address'
|
||||
: type === 'vote'
|
||||
? 'Submit your vote for the proposal'
|
||||
: 'Delegate your voting power to another address'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!txHash ? (
|
||||
<div className="space-y-4">
|
||||
{type === 'send' && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="recipient">Recipient Address</Label>
|
||||
<Input
|
||||
id="recipient"
|
||||
placeholder="0x..."
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="amount">Amount (PZK)</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
placeholder="0.0"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'vote' && (
|
||||
<div>
|
||||
<Label htmlFor="message">Vote Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Enter your vote reason (optional)"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={type === 'send' ? handleSendTransaction : handleSignMessage}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-kesk hover:bg-kesk/90"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{type === 'send' ? 'Send Transaction' : 'Sign & Submit'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Alert className="border-kesk/20">
|
||||
<CheckCircle className="h-4 w-4 text-kesk" />
|
||||
<AlertDescription>
|
||||
Transaction submitted successfully!
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-sm text-muted-foreground">Transaction Hash</div>
|
||||
<div className="font-mono text-xs break-all">{txHash}</div>
|
||||
</div>
|
||||
<Button onClick={resetForm} className="w-full">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { Wallet, LogOut, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { formatAddress, formatBalance } from '@/lib/wallet';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export const WalletButton: React.FC = () => {
|
||||
const {
|
||||
isConnected,
|
||||
address,
|
||||
balance,
|
||||
chainId,
|
||||
error,
|
||||
connectMetaMask,
|
||||
disconnect,
|
||||
switchNetwork
|
||||
} = useWallet();
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-sor text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={connectMetaMask}
|
||||
className="bg-kesk hover:bg-kesk/90 text-white"
|
||||
>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isCorrectNetwork = chainId === '0x2329';
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="border-kesk/20 hover:border-kesk">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="h-4 w-4 text-kesk" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium">{formatAddress(address!)}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatBalance(balance)} PZK</div>
|
||||
</div>
|
||||
{!isCorrectNetwork && (
|
||||
<Badge variant="destructive" className="ml-2 bg-sor">
|
||||
Wrong Network
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel>Wallet Details</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Address</div>
|
||||
<div className="text-sm font-mono">{formatAddress(address!)}</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Balance</div>
|
||||
<div className="text-sm font-medium">{formatBalance(balance)} PZK</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="text-sm text-muted-foreground">Network</div>
|
||||
<div className="text-sm font-medium">
|
||||
{isCorrectNetwork ? 'PezkuwiChain' : 'Unknown Network'}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
{!isCorrectNetwork && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={switchNetwork} className="text-zer">
|
||||
Switch to PezkuwiChain
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={disconnect} className="text-sor">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Disconnect
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Wallet, Chrome, Smartphone, Copy, Check, ExternalLink } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { formatAddress } from '@/lib/wallet';
|
||||
|
||||
interface WalletModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) => {
|
||||
const { connectMetaMask, connectWalletConnect, isConnected, address } = useWallet();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyAddress = () => {
|
||||
if (address) {
|
||||
navigator.clipboard.writeText(address);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMetaMaskConnect = async () => {
|
||||
await connectMetaMask();
|
||||
if (isConnected) onClose();
|
||||
};
|
||||
|
||||
const handleWalletConnectConnect = async () => {
|
||||
await connectWalletConnect();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5 text-kesk" />
|
||||
Connect Wallet
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect your wallet to interact with PezkuwiChain governance
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!isConnected ? (
|
||||
<Tabs defaultValue="browser" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="browser">Browser Wallet</TabsTrigger>
|
||||
<TabsTrigger value="mobile">Mobile Wallet</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="browser" className="space-y-4">
|
||||
<Button
|
||||
onClick={handleMetaMaskConnect}
|
||||
className="w-full justify-start bg-kesk hover:bg-kesk/90"
|
||||
>
|
||||
<Chrome className="mr-2 h-5 w-5" />
|
||||
MetaMask
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Don't have MetaMask?{' '}
|
||||
<a
|
||||
href="https://metamask.io/download/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-kesk hover:underline"
|
||||
>
|
||||
Download here
|
||||
</a>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="mobile" className="space-y-4">
|
||||
<Button
|
||||
onClick={handleWalletConnectConnect}
|
||||
className="w-full justify-start bg-zer hover:bg-zer/90"
|
||||
>
|
||||
<Smartphone className="mr-2 h-5 w-5" />
|
||||
WalletConnect
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Scan QR code with your mobile wallet to connect
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Connected Address</div>
|
||||
<div className="font-mono font-medium">{formatAddress(address!)}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleCopyAddress}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-kesk" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => window.open('https://explorer.pezkuwichain.app/address/' + address, '_blank')}
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
View on Explorer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user