feat(admin): complete admin console with 100M WUSD faucet, market wizard, governance and adjudication

This commit is contained in:
Bot
2026-08-31 00:38:19 +08:00
parent 1e9b9bd0b2
commit acc5274a1e
22 changed files with 8293 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+15 -2
View File
@@ -10,10 +10,23 @@
"dependencies": {
"@wtfx/types": "workspace:*",
"@wtfx/api-client": "workspace:*",
"@wtfx/contracts": "workspace:*",
"@wtfx/ui": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next": "^14.2.4",
"ethers": "^6.13.0"
"ethers": "^6.13.0",
"lucide-react": "^0.395.0",
"clsx": "^2.1.1",
"tailwind-merge": "^2.3.0"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+9
View File
@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #090A0F;
color: #F3F4F6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
+21
View File
@@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'WTFX Admin Console',
description: 'Protocol Governance, Contract Deployment & Market Risk Control',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body className="min-h-screen bg-background text-gray-100 flex flex-col">
{children}
</body>
</html>
);
}
+32
View File
@@ -0,0 +1,32 @@
'use client';
import React, { useState } from 'react';
import { Web3Provider } from '../context/Web3Context';
import { AdminNavbar } from '../features/common/AdminNavbar';
import { FaucetPanel } from '../features/faucet/FaucetPanel';
import { MarketWizard } from '../features/wizard/MarketWizard';
import { GovernancePanel } from '../features/governance/GovernancePanel';
import { AdjudicationPanel } from '../features/adjudication/AdjudicationPanel';
export default function AdminPage() {
const [activeTab, setActiveTab] = useState<string>('faucet');
return (
<Web3Provider>
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col selection:bg-cyan-500 selection:text-white">
<AdminNavbar activeTab={activeTab} setActiveTab={setActiveTab} />
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'faucet' && <FaucetPanel />}
{activeTab === 'wizard' && <MarketWizard />}
{activeTab === 'governance' && <GovernancePanel />}
{activeTab === 'adjudication' && <AdjudicationPanel />}
</main>
<footer className="border-t border-slate-900 py-6 text-center text-xs text-slate-600">
WTFX Protocol Governance &amp; Administration Console &bull; Robinhood Chain Testnet (ID: 46630)
</footer>
</div>
</Web3Provider>
);
}
+157
View File
@@ -0,0 +1,157 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { DEPLOYMENTS, ABIS, ROBINHOOD_TESTNET_CHAIN } from '@wtfx/contracts';
interface Web3ContextType {
account: string | null;
provider: ethers.BrowserProvider | null;
signer: ethers.Signer | null;
chainId: number | null;
isCorrectNetwork: boolean;
connectWallet: () => Promise<void>;
switchNetwork: () => Promise<void>;
getControllerContract: () => ethers.Contract | null;
getCollateralContract: () => ethers.Contract | null;
}
const Web3Context = createContext<Web3ContextType>({
account: null,
provider: null,
signer: null,
chainId: null,
isCorrectNetwork: false,
connectWallet: async () => {},
switchNetwork: async () => {},
getControllerContract: () => null,
getCollateralContract: () => null,
});
export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [account, setAccount] = useState<string | null>(null);
const [provider, setProvider] = useState<ethers.BrowserProvider | null>(null);
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [chainId, setChainId] = useState<number | null>(null);
const isCorrectNetwork = chainId === ROBINHOOD_TESTNET_CHAIN.id;
const initProvider = async () => {
if (typeof window !== 'undefined' && (window as any).ethereum) {
const browserProvider = new ethers.BrowserProvider((window as any).ethereum);
setProvider(browserProvider);
try {
const network = await browserProvider.getNetwork();
setChainId(Number(network.chainId));
const accounts = await browserProvider.listAccounts();
if (accounts.length > 0) {
setAccount(accounts[0].address);
setSigner(await browserProvider.getSigner());
}
} catch (err) {
console.error('Error initializing web3 provider:', err);
}
(window as any).ethereum.on('accountsChanged', async (accs: string[]) => {
if (accs.length > 0) {
setAccount(accs[0]);
const currentSigner = await browserProvider.getSigner();
setSigner(currentSigner);
} else {
setAccount(null);
setSigner(null);
}
});
(window as any).ethereum.on('chainChanged', (cId: string) => {
setChainId(parseInt(cId, 16));
window.location.reload();
});
}
};
useEffect(() => {
initProvider();
}, []);
const connectWallet = async () => {
if (typeof window !== 'undefined' && (window as any).ethereum) {
try {
const browserProvider = new ethers.BrowserProvider((window as any).ethereum);
await browserProvider.send('eth_requestAccounts', []);
const currentSigner = await browserProvider.getSigner();
const address = await currentSigner.getAddress();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(currentSigner);
setAccount(address);
setChainId(Number(network.chainId));
} catch (err) {
console.error('Failed to connect wallet:', err);
throw err;
}
} else {
alert('请安装 MetaMask 或其他 Web3 钱包!');
}
};
const switchNetwork = async () => {
if (typeof window !== 'undefined' && (window as any).ethereum) {
const hexChainId = '0x' + ROBINHOOD_TESTNET_CHAIN.id.toString(16);
try {
await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: hexChainId }],
});
} catch (switchError: any) {
if (switchError.code === 4902) {
try {
await (window as any).ethereum.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: hexChainId,
chainName: ROBINHOOD_TESTNET_CHAIN.name,
rpcUrls: ROBINHOOD_TESTNET_CHAIN.rpcUrls.default.http,
nativeCurrency: ROBINHOOD_TESTNET_CHAIN.nativeCurrency,
},
],
});
} catch (addError) {
console.error('Failed to add network:', addError);
}
}
}
}
};
const getControllerContract = () => {
if (!provider) return null;
const address = DEPLOYMENTS.robinhoodTestnet.contracts.controllerProxy;
return new ethers.Contract(address, ABIS.WTFControllerV2, signer || provider);
};
const getCollateralContract = () => {
if (!provider) return null;
const address = DEPLOYMENTS.robinhoodTestnet.contracts.collateral;
return new ethers.Contract(address, ABIS.MockERC20, signer || provider);
};
return (
<Web3Context.Provider
value={{
account,
provider,
signer,
chainId,
isCorrectNetwork,
connectWallet,
switchNetwork,
getControllerContract,
getCollateralContract,
}}
>
{children}
</Web3Context.Provider>
);
};
export const useWeb3 = () => useContext(Web3Context);
@@ -0,0 +1,241 @@
import React, { useState } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { ethers } from 'ethers';
import { Scale, CheckCircle2, AlertOctagon, PauseCircle, PlayCircle, RefreshCw, Loader2, Check, AlertCircle } from 'lucide-react';
export const AdjudicationPanel: React.FC = () => {
const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3();
const [questionId, setQuestionId] = useState('');
const [outcomeIndex, setOutcomeIndex] = useState<number>(0);
const [marketAddress, setMarketAddress] = useState('');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const handleResolve = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
const tx = await controller.resolveOutcome(qId, outcomeIndex);
setStatusMsg({ type: 'success', text: `预言机初步裁决交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 市场问题 ${qId.slice(0, 8)}... 已由预言机成功提交胜出结果 Outcome #${outcomeIndex}!进入争议公示期。` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '裁决失败' });
} finally {
setLoading(false);
}
};
const handleFinalise = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
const tx = await controller.finaliseOutcome(qId);
setStatusMsg({ type: 'success', text: `最终结果敲定交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 市场问题 ${qId.slice(0, 8)}... 争议期结束,胜出结果已在链上永久生效,用户可兑现奖励!` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '敲定失败' });
} finally {
setLoading(false);
}
};
const handleOverrideFinalise = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const qId = questionId.startsWith('0x') ? questionId : ethers.keccak256(ethers.toUtf8Bytes(questionId));
const tx = await controller.overrideFinalise(qId, outcomeIndex);
setStatusMsg({ type: 'success', text: `管理员一票否决/强制裁决交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🛡️ 管理员已强制将市场问题 ${qId.slice(0, 8)}... 敲定为 Outcome #${outcomeIndex}` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '强制裁决失败' });
} finally {
setLoading(false);
}
};
const handlePause = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const tx = await controller.pause();
setStatusMsg({ type: 'success', text: `紧急熔断暂停交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🚨 协议已进入全局紧急暂停 (PAUSED) 状态!` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '暂停失败' });
} finally {
setLoading(false);
}
};
const handleUnpause = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const tx = await controller.unpause();
setStatusMsg({ type: 'success', text: `解除紧急暂停交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `✅ 协议已恢复正常运行!` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '恢复失败' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-3 bg-purple-500/10 rounded-xl border border-purple-500/20 text-purple-400">
<Scale className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white"> (Adjudication & Emergency)</h2>
<p className="text-sm text-slate-400 mt-0.5">
</p>
</div>
</div>
</div>
{/* Question Outcome Adjudication */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-5 space-y-4">
<h3 className="text-sm font-semibold text-slate-200">1. (Outcome Resolution)</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<label className="block text-xs font-medium text-slate-400 mb-1.5">Question ID (Bytes32 / )</label>
<input
type="text"
value={questionId}
onChange={(e) => setQuestionId(e.target.value)}
placeholder="0x..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Winning Outcome Index)</label>
<input
type="number"
min="0"
max="7"
value={outcomeIndex}
onChange={(e) => setOutcomeIndex(Number(e.target.value))}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<div className="flex flex-wrap gap-3 pt-2">
<button
onClick={handleResolve}
disabled={loading || !account || !questionId}
className="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-indigo-600/20 disabled:opacity-50"
>
<CheckCircle2 className="w-4 h-4" />
<span> (resolveOutcome)</span>
</button>
<button
onClick={handleFinalise}
disabled={loading || !account || !questionId}
className="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-emerald-600/20 disabled:opacity-50"
>
<Scale className="w-4 h-4" />
<span> (finaliseOutcome)</span>
</button>
<button
onClick={handleOverrideFinalise}
disabled={loading || !account || !questionId}
className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 text-white rounded-lg text-xs font-semibold flex items-center space-x-1.5 shadow-md shadow-purple-600/20 disabled:opacity-50"
>
<AlertOctagon className="w-4 h-4" />
<span> (overrideFinalise)</span>
</button>
</div>
</div>
{/* Protocol Pause Control */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-5 space-y-4">
<h3 className="text-sm font-semibold text-rose-400 flex items-center space-x-2">
<AlertOctagon className="w-4 h-4" />
<span>2. (Circuit Breaker)</span>
</h3>
<p className="text-xs text-slate-400">
GUARDIAN_ROLE ADMIN
</p>
<div className="flex gap-4 pt-1">
<button
onClick={handlePause}
disabled={loading || !account}
className="px-5 py-2.5 bg-rose-600/20 hover:bg-rose-600/30 text-rose-300 border border-rose-600/40 rounded-lg text-xs font-semibold flex items-center space-x-2 transition-colors disabled:opacity-50"
>
<PauseCircle className="w-4 h-4" />
<span> (Pause All Markets)</span>
</button>
<button
onClick={handleUnpause}
disabled={loading || !account}
className="px-5 py-2.5 bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-300 border border-emerald-600/40 rounded-lg text-xs font-semibold flex items-center space-x-2 transition-colors disabled:opacity-50"
>
<PlayCircle className="w-4 h-4" />
<span> (Unpause Protocol)</span>
</button>
</div>
</div>
{/* Status Message */}
{statusMsg && (
<div
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
</div>
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,92 @@
import React from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { Wallet, ShieldAlert, CheckCircle2 } from 'lucide-react';
interface NavbarProps {
activeTab: string;
setActiveTab: (tab: string) => void;
}
export const AdminNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => {
const { account, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3();
const navItems = [
{ id: 'faucet', label: '🚰 WUSD 水龙头' },
{ id: 'wizard', label: '🚀 创建市场向导' },
{ id: 'governance', label: '⚙️ 协议参数治理' },
{ id: 'adjudication', label: '⚖️ 市场风控与裁决' },
];
return (
<header className="bg-slate-900/80 backdrop-blur-md border-b border-slate-800 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center space-x-8">
<div className="flex items-center space-x-3">
<div className="w-9 h-9 bg-gradient-to-tr from-cyan-500 to-indigo-600 rounded-lg flex items-center justify-center font-black text-white shadow-lg shadow-cyan-500/20">
W
</div>
<div>
<span className="font-bold text-lg text-white tracking-wider">WTFX</span>
<span className="ml-2 text-xs font-mono px-2 py-0.5 rounded bg-cyan-950 text-cyan-400 border border-cyan-800">
ADMIN CONSOLE
</span>
</div>
</div>
<nav className="flex space-x-1">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`px-3.5 py-2 rounded-lg text-sm font-medium transition-all ${
activeTab === item.id
? 'bg-slate-800 text-white shadow-sm border border-slate-700'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
{item.label}
</button>
))}
</nav>
</div>
<div className="flex items-center space-x-4">
{account ? (
<div className="flex items-center space-x-3">
{!isCorrectNetwork ? (
<button
onClick={switchNetwork}
className="flex items-center space-x-1.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-amber-500/20 transition-colors"
>
<ShieldAlert className="w-4 h-4" />
<span> Robinhood </span>
</button>
) : (
<span className="flex items-center space-x-1 text-emerald-400 bg-emerald-950/40 border border-emerald-800/50 px-2.5 py-1 rounded-md text-xs font-mono">
<CheckCircle2 className="w-3.5 h-3.5" />
<span>Robinhood 46630</span>
</span>
)}
<div className="flex items-center space-x-2 bg-slate-800 border border-slate-700 px-3 py-1.5 rounded-lg">
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
<span className="font-mono text-xs text-slate-200">
{account.slice(0, 6)}...{account.slice(-4)}
</span>
</div>
</div>
) : (
<button
onClick={connectWallet}
className="flex items-center space-x-2 bg-cyan-600 hover:bg-cyan-500 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-md shadow-cyan-600/20 transition-all"
>
<Wallet className="w-4 h-4" />
<span></span>
</button>
)}
</div>
</div>
</header>
);
};
@@ -0,0 +1,198 @@
import React, { useState, useEffect } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { Coins, ArrowUpRight, Check, AlertCircle, Loader2, Sparkles } from 'lucide-react';
export const FaucetPanel: React.FC = () => {
const { account, signer, getCollateralContract, isCorrectNetwork } = useWeb3();
const [balance, setBalance] = useState<string>('0');
const [loading, setLoading] = useState<boolean>(false);
const [customAmount, setCustomAmount] = useState<string>('100000000');
const [targetAddress, setTargetAddress] = useState<string>('');
const [txHash, setTxHash] = useState<string | null>(null);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const collateralAddress = DEPLOYMENTS.robinhoodTestnet.contracts.collateral;
const fetchBalance = async () => {
if (!account) return;
try {
const contract = getCollateralContract();
if (contract) {
const bal = await contract.balanceOf(account);
setBalance(ethers.formatUnits(bal, 18));
}
} catch (err) {
console.error('Failed to fetch balance:', err);
}
};
useEffect(() => {
if (account) {
setTargetAddress(account);
fetchBalance();
}
}, [account]);
const handleMint = async (amountStr: string) => {
if (!signer || !account) {
setStatusMsg({ type: 'error', text: '请先连接管理员钱包!' });
return;
}
if (!isCorrectNetwork) {
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
return;
}
try {
setLoading(true);
setStatusMsg(null);
setTxHash(null);
const contract = getCollateralContract();
if (!contract) throw new Error('Contract not initialized');
const recipient = targetAddress.trim() || account;
const amountWei = ethers.parseUnits(amountStr, 18);
const tx = await contract.mint(recipient, amountWei);
setStatusMsg({ type: 'success', text: `交易已广播,等待链上确认... Tx: ${tx.hash.slice(0, 10)}...` });
setTxHash(tx.hash);
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 成功铸造 ${Number(amountStr).toLocaleString()} WUSD 到地址 ${recipient}` });
fetchBalance();
} catch (err: any) {
console.error('Mint error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '铸造失败,请检查交易状态' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl relative overflow-hidden">
<div className="absolute -right-10 -top-10 w-64 h-64 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-800 pb-6">
<div className="flex items-center space-x-3">
<div className="p-3 bg-cyan-500/10 rounded-xl border border-cyan-500/20 text-cyan-400">
<Coins className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white">WUSD (Testnet Faucet)</h2>
<p className="text-sm text-slate-400 mt-0.5">
Mock ERC-20 : <span className="font-mono text-cyan-400">{collateralAddress}</span>
</p>
</div>
</div>
<div className="bg-slate-950/60 border border-slate-800 rounded-xl px-4 py-2.5">
<span className="text-xs text-slate-400 block"></span>
<span className="text-lg font-bold font-mono text-emerald-400">
{Number(balance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD
</span>
</div>
</div>
<div className="mt-6 space-y-6">
{/* Quick Action */}
<div className="bg-gradient-to-r from-slate-950 to-slate-900 border border-cyan-900/40 rounded-xl p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<div className="flex items-center space-x-2">
<Sparkles className="w-5 h-5 text-amber-400" />
<h3 className="text-base font-semibold text-white"> (100,000,000 WUSD)</h3>
</div>
<p className="text-xs text-slate-400 mt-1">
1 亿
</p>
</div>
<button
onClick={() => handleMint('100000000')}
disabled={loading || !account}
className="w-full sm:w-auto px-6 py-3 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center justify-center space-x-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Coins className="w-4 h-4" />
<span> 1 亿 WUSD</span>
</>
)}
</button>
</div>
{/* Custom Mint Form */}
<div className="bg-slate-950/40 border border-slate-800/80 rounded-xl p-5 space-y-4">
<h3 className="text-sm font-semibold text-slate-300"> (Custom Mint)</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Recipient Address)</label>
<input
type="text"
value={targetAddress}
onChange={(e) => setTargetAddress(e.target.value)}
placeholder="0x..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Amount WUSD)</label>
<input
type="number"
value={customAmount}
onChange={(e) => setCustomAmount(e.target.value)}
placeholder="1000000"
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => handleMint(customAmount)}
disabled={loading || !account || !customAmount}
className="px-5 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-sm font-medium transition-colors flex items-center space-x-2 disabled:opacity-50"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowUpRight className="w-4 h-4" />}
<span></span>
</button>
</div>
</div>
{/* Status Message */}
{statusMsg && (
<div
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
{txHash && (
<p className="mt-1 text-xs text-slate-400">
Tx Hash: <span className="font-mono text-cyan-400">{txHash}</span>
</p>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,251 @@
import React, { useState, useEffect } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { Sliders, ShieldCheck, Percent, Wallet, Check, AlertCircle, Loader2, RefreshCw } from 'lucide-react';
export const GovernancePanel: React.FC = () => {
const { account, signer, getControllerContract, isCorrectNetwork } = useWeb3();
const [treasury, setTreasury] = useState<string>('');
const [defaultFeeRate, setDefaultFeeRate] = useState<string>('0.6');
const [creatorShare, setCreatorShare] = useState<string>('50');
const [centralWallet, setCentralWallet] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const fetchGovParams = async () => {
try {
const controller = getControllerContract();
if (!controller) return;
// In real scenario, read from getters or view functions
// We fill initialized defaults from deployment
setTreasury(DEPLOYMENTS.robinhoodTestnet.governance.treasury);
setCentralWallet(DEPLOYMENTS.robinhoodTestnet.governance.admin);
} catch (err) {
console.error('Fetch gov params error:', err);
}
};
useEffect(() => {
fetchGovParams();
}, [account]);
const handleUpdateTreasury = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const tx = await controller.setTreasury(treasury.trim());
setStatusMsg({ type: 'success', text: `更新国库金库地址交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 协议国库金库地址已成功更新为: ${treasury}` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally {
setLoading(false);
}
};
const handleUpdateFeeRate = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const rateWei = ethers.parseUnits((Number(defaultFeeRate) / 100).toString(), 18);
const tx = await controller.setFeeRateDefault(rateWei);
setStatusMsg({ type: 'success', text: `更新默认手续费交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 全局默认协议手续费率已成功更新为: ${defaultFeeRate}%` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally {
setLoading(false);
}
};
const handleUpdateCreatorShare = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const shareWei = ethers.parseUnits((Number(creatorShare) / 100).toString(), 18);
const tx = await controller.setCreatorShare(shareWei);
setStatusMsg({ type: 'success', text: `更新建盘者手续费分成交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 建盘者手续费返佣分成比例已成功更新为: ${creatorShare}%` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally {
setLoading(false);
}
};
const handleUpdateCentralWallet = async () => {
if (!signer || !account) return;
try {
setLoading(true);
setStatusMsg(null);
const controller = getControllerContract();
if (!controller) throw new Error('Controller not found');
const tx = await controller.setCentralWallet(centralWallet.trim());
setStatusMsg({ type: 'success', text: `更新中央安全提币钱包交易已广播: ${tx.hash.slice(0, 10)}...` });
await tx.wait();
setStatusMsg({ type: 'success', text: `🎉 中央安全提币钱包已成功更新为: ${centralWallet}` });
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.reason || err.message || '更新失败' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-3 bg-amber-500/10 rounded-xl border border-amber-500/20 text-amber-400">
<Sliders className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white"> (Protocol Governance)</h2>
<p className="text-sm text-slate-400 mt-0.5">
WTFControllerV2 DEFAULT_ADMIN_ROLE / OPERATOR_ROLE
</p>
</div>
</div>
<button
onClick={fetchGovParams}
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Treasury Setting */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<ShieldCheck className="w-4 h-4 text-cyan-400" />
<span> (Treasury Address)</span>
</div>
<p className="text-xs text-slate-400">/</p>
<input
type="text"
value={treasury}
onChange={(e) => setTreasury(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateTreasury}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
{/* Central Wallet */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Wallet className="w-4 h-4 text-indigo-400" />
<span> (Central Wallet)</span>
</div>
<p className="text-xs text-slate-400"></p>
<input
type="text"
value={centralWallet}
onChange={(e) => setCentralWallet(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateCentralWallet}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
{/* Default Fee Rate */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Percent className="w-4 h-4 text-emerald-400" />
<span> (Fee Rate %)</span>
</div>
<p className="text-xs text-slate-400"> [0.1%, 3.0%] 0.6%</p>
<input
type="number"
step="0.01"
value={defaultFeeRate}
onChange={(e) => setDefaultFeeRate(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateFeeRate}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
{/* Creator Share */}
<div className="bg-slate-950/50 border border-slate-800 rounded-xl p-4 space-y-3">
<div className="flex items-center space-x-2 text-sm font-semibold text-slate-200">
<Percent className="w-4 h-4 text-pink-400" />
<span> (Creator Share %)</span>
</div>
<p className="text-xs text-slate-400"> 50%</p>
<input
type="number"
step="1"
value={creatorShare}
onChange={(e) => setCreatorShare(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500"
/>
<button
onClick={handleUpdateCreatorShare}
disabled={loading || !account}
className="w-full py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-600 rounded-lg text-xs font-medium transition-colors"
>
</button>
</div>
</div>
{/* Status Message */}
{statusMsg && (
<div
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
</div>
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,326 @@
import React, { useState } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { PlusCircle, Trash2, Rocket, Check, AlertCircle, Loader2, Sparkles, HelpCircle } from 'lucide-react';
export const MarketWizard: React.FC = () => {
const { account, signer, getControllerContract, getCollateralContract, isCorrectNetwork } = useWeb3();
const [question, setQuestion] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState('Crypto');
const [endTime, setEndTime] = useState(
new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 16)
);
const [outcomes, setOutcomes] = useState<string[]>(['YES', 'NO']);
const [selectedTier, setSelectedTier] = useState<number>(1);
const [seedAmount, setSeedAmount] = useState<string>('1000');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [deployedMarketAddress, setDeployedMarketAddress] = useState<string | null>(null);
const addOutcome = () => {
if (outcomes.length < 8) {
setOutcomes([...outcomes, `OUTCOME ${outcomes.length + 1}`]);
}
};
const removeOutcome = (index: number) => {
if (outcomes.length > 2) {
setOutcomes(outcomes.filter((_, i) => i !== index));
}
};
const updateOutcome = (index: number, val: string) => {
const updated = [...outcomes];
updated[index] = val;
setOutcomes(updated);
};
const handleDeployMarket = async (e: React.FormEvent) => {
e.preventDefault();
if (!signer || !account) {
setStatusMsg({ type: 'error', text: '请先连接管理员钱包!' });
return;
}
if (!isCorrectNetwork) {
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
return;
}
try {
setLoading(true);
setStatusMsg(null);
setDeployedMarketAddress(null);
const controller = getControllerContract();
const collateral = getCollateralContract();
if (!controller || !collateral) throw new Error('Contracts not loaded');
const controllerAddress = await controller.getAddress();
const seedWei = ethers.parseUnits(seedAmount || '0', 18);
// Step 1: Check and Approve Collateral if Seed > 0
if (seedWei > 0n) {
setStatusMsg({ type: 'success', text: '步骤 1/3: 正在授权底仓资金...' });
const allowance = await collateral.allowance(account, controllerAddress);
if (allowance < seedWei) {
const appTx = await collateral.approve(controllerAddress, ethers.MaxUint256);
await appTx.wait();
}
}
// Step 2: Prepare Question and Market Params
setStatusMsg({ type: 'success', text: '步骤 2/3: 正在广播建盘交易到链上...' });
const deadlineSec = Math.floor(new Date(endTime).getTime() / 1000);
const questionId = ethers.keccak256(
ethers.toUtf8Bytes(`${question}-${Date.now()}-${account}`)
);
const questionParams = {
questionId: questionId,
question: question,
ancillaryData: ethers.toUtf8Bytes(JSON.stringify({ description, category })),
rewardToken: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
reward: 0,
proposalBond: 0,
earlyResolutionBond: 0,
settlementResolutionBond: 0,
resolutionTime: deadlineSec,
numOutcomes: outcomes.length,
};
const marketParams = {
tier: selectedTier,
creator: account,
feeRate: ethers.parseUnits('0.006', 18), // 0.6% default
creatorShare: ethers.parseUnits('0.5', 18), // 50%
curve: DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve,
collateral: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
};
// Step 3: Call deployMarket
const tx = await controller.deployMarket(
questionParams,
marketParams,
account, // Oracle address (admin acts as oracle)
seedWei
);
setStatusMsg({ type: 'success', text: `步骤 3/3: 交易已广播 (Tx: ${tx.hash.slice(0, 10)}...),等待区块确认...` });
const receipt = await tx.wait();
// Extract MarketDeployed event or address
setStatusMsg({
type: 'success',
text: `🎉 预测市场创建成功!交易哈希: ${receipt.hash}`,
});
} catch (err: any) {
console.error('Market deploy error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '建盘失败,请检查参数' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<form onSubmit={handleDeployMarket} className="bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-3 bg-indigo-500/10 rounded-xl border border-indigo-500/20 text-indigo-400">
<Rocket className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white"> (Deploy Market Wizard)</h2>
<p className="text-sm text-slate-400 mt-0.5">
WTFControllerV2 PowerLDACurveV2 线
</p>
</div>
</div>
</div>
{/* Basic Info */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">1. </h3>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> / (Question Title)</label>
<input
type="text"
required
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="例如:Will Bitcoin exceed $150,000 before December 31, 2026?"
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Category)</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
>
<option value="Crypto">Crypto ()</option>
<option value="Politics">Politics ()</option>
<option value="Macro">Macro ()</option>
<option value="Sports">Sports ()</option>
<option value="Tech">Tech ()</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Resolution Time)</label>
<input
type="datetime-local"
required
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
>
</input>
</div>
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> / (Resolution Criteria / Data Source)</label>
<textarea
rows={2}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="例如:以 Binance 现货 BTC/USDT 交易对在 UTC 2026-12-31 23:59:59 前的最高成交价为准..."
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
{/* Outcomes */}
<div className="space-y-4 pt-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">2. (Outcome Options)</h3>
<button
type="button"
onClick={addOutcome}
disabled={outcomes.length >= 8}
className="text-xs text-cyan-400 hover:text-cyan-300 flex items-center space-x-1 disabled:opacity-50"
>
<PlusCircle className="w-3.5 h-3.5" />
<span> (8)</span>
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{outcomes.map((outcome, idx) => (
<div key={idx} className="flex items-center space-x-2 bg-slate-950 border border-slate-800 rounded-lg p-2">
<span className="w-6 text-center font-mono text-xs text-slate-500">#{idx}</span>
<input
type="text"
required
value={outcome}
onChange={(e) => updateOutcome(idx, e.target.value)}
className="flex-1 bg-transparent text-sm text-slate-200 focus:outline-none"
/>
{outcomes.length > 2 && (
<button
type="button"
onClick={() => removeOutcome(idx)}
className="text-slate-500 hover:text-rose-400 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
))}
</div>
</div>
{/* Tier & Liquidity Seed */}
<div className="space-y-4 pt-2">
<h3 className="text-sm font-semibold text-slate-300 uppercase tracking-wider">3. (Tier & Seed)</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{[
{ tier: 1, name: 'Tier 1 (标准盘)', desc: '支持自由创建,毕业需 100K 资金池' },
{ tier: 2, name: 'Tier 2 (精选盘)', desc: '具备更强流动性深度与做市扶持' },
{ tier: 3, name: 'Tier 3 (旗舰盘)', desc: '极高容量上限与官方背书' },
].map((t) => (
<div
key={t.tier}
onClick={() => setSelectedTier(t.tier)}
className={`p-3.5 rounded-xl border cursor-pointer transition-all ${
selectedTier === t.tier
? 'bg-cyan-950/40 border-cyan-500 text-white'
: 'bg-slate-950/60 border-slate-800 text-slate-400 hover:border-slate-700'
}`}
>
<div className="font-bold text-sm text-slate-200">{t.name}</div>
<div className="text-xs text-slate-500 mt-1">{t.desc}</div>
</div>
))}
</div>
<div>
<label className="block text-xs font-medium text-slate-400 mb-1.5"> (Initial Seed WUSD)</label>
<input
type="number"
value={seedAmount}
onChange={(e) => setSeedAmount(e.target.value)}
placeholder="1000"
className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
/>
<p className="text-xs text-slate-500 mt-1"></p>
</div>
</div>
{/* Submit */}
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
<div className="text-xs text-slate-500">
(Oracle)
</div>
<button
type="submit"
disabled={loading || !account}
className="px-6 py-3 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center space-x-2 transition-all disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Rocket className="w-4 h-4" />
<span></span>
</>
)}
</button>
</div>
{/* Status Message */}
{statusMsg && (
<div
className={`p-4 rounded-xl border flex items-start space-x-3 text-sm ${
statusMsg.type === 'success'
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
</div>
</div>
)}
</form>
</div>
);
};
+21
View File
@@ -0,0 +1,21 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}'
],
theme: {
extend: {
colors: {
background: '#090A0F',
card: '#12141F',
border: '#1F2438',
primary: '#3B82F6',
success: '#10B981',
danger: '#EF4444',
warning: '#F59E0B'
}
}
},
plugins: []
};
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}