feat(web): add apps/web Next.js app src, tailwind, tsconfig and pages
This commit is contained in:
Vendored
+5
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background-color: #090A0F;
|
||||
color: #F3F4F6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import './globals.css';
|
||||
import React from 'react';
|
||||
|
||||
export const metadata = {
|
||||
title: 'WTFX | Next-Gen Prediction Market & Liquidity Protocol',
|
||||
description: 'Ultra-low latency prediction markets powered by Power LDA Bonding Curves',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN" className="dark">
|
||||
<body className="bg-[#090A0F] text-slate-100 antialiased min-h-screen">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Web3Provider } from '../context/Web3Context';
|
||||
import { WebNavbar } from '../components/WebNavbar';
|
||||
import { MarketExplore, MarketItem } from '../features/explore/MarketExplore';
|
||||
import { TradingTerminal } from '../features/trading/TradingTerminal';
|
||||
import { PortfolioView } from '../features/portfolio/PortfolioView';
|
||||
|
||||
const DEFAULT_MARKET: MarketItem = {
|
||||
id: 'm-1',
|
||||
marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a',
|
||||
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||
category: 'Crypto',
|
||||
tier: 1,
|
||||
outcomes: [
|
||||
{ label: 'YES', price: 0.68, percentage: 68 },
|
||||
{ label: 'NO', price: 0.32, percentage: 32 }
|
||||
],
|
||||
poolLiquidity: 1250000,
|
||||
volume24h: 384000,
|
||||
endTime: '2026-12-31',
|
||||
status: 'ACTIVE'
|
||||
};
|
||||
|
||||
export default function WebApp() {
|
||||
const [activeTab, setActiveTab] = useState<string>('explore');
|
||||
const [selectedMarket, setSelectedMarket] = useState<MarketItem>(DEFAULT_MARKET);
|
||||
|
||||
const handleSelectMarket = (m: MarketItem) => {
|
||||
setSelectedMarket(m);
|
||||
setActiveTab('trade');
|
||||
};
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<div className="min-h-screen bg-[#090A0F] text-slate-100 flex flex-col selection:bg-cyan-500 selection:text-white">
|
||||
<WebNavbar 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 === 'explore' && <MarketExplore onSelectMarket={handleSelectMarket} />}
|
||||
{activeTab === 'trade' && <TradingTerminal market={selectedMarket} />}
|
||||
{activeTab === 'portfolio' && <PortfolioView />}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-slate-900 py-8 text-center text-xs text-slate-600 space-y-2">
|
||||
<div>
|
||||
WTFX Prediction Markets • Powered by Power LDA Bonding Curves on Robinhood Chain (ID: 46630)
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-700">
|
||||
All positions and ledger settlements are guaranteed deterministically.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Web3Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { useWeb3 } from '../context/Web3Context';
|
||||
import { Wallet, TrendingUp, Compass, PieChart, ShieldAlert, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => {
|
||||
const { account, wusdBalance, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3();
|
||||
|
||||
const navs = [
|
||||
{ id: 'explore', label: '🔥 市场探索', icon: Compass },
|
||||
{ id: 'trade', label: '⚡ 交易终端', icon: TrendingUp },
|
||||
{ id: 'portfolio', label: '💼 我的持仓与账本', icon: PieChart },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-[#0c0e17]/90 backdrop-blur-md border-b border-slate-800/80 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">
|
||||
{/* Logo & Navigation */}
|
||||
<div className="flex items-center space-x-8">
|
||||
<div className="flex items-center space-x-3 cursor-pointer" onClick={() => setActiveTab('explore')}>
|
||||
<div className="w-9 h-9 bg-gradient-to-tr from-cyan-400 via-indigo-500 to-purple-600 rounded-xl flex items-center justify-center font-black text-white shadow-lg shadow-cyan-500/20">
|
||||
W
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-1.5">
|
||||
<span className="font-extrabold text-xl text-white tracking-wider bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-300">
|
||||
WTFX
|
||||
</span>
|
||||
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded bg-gradient-to-r from-cyan-500/20 to-purple-500/20 text-cyan-300 border border-cyan-500/30">
|
||||
POWER LDA
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex space-x-1">
|
||||
{navs.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${
|
||||
isActive
|
||||
? 'bg-slate-800/80 text-white shadow-sm border border-slate-700/80'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/40'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-4 h-4 ${isActive ? 'text-cyan-400' : 'text-slate-500'}`} />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Right Section / Wallet Status */}
|
||||
<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>
|
||||
) : (
|
||||
<div className="hidden sm:flex items-center space-x-2 bg-slate-900/80 border border-slate-800 px-3 py-1.5 rounded-xl">
|
||||
<span className="text-xs text-slate-400">可用:</span>
|
||||
<span className="text-xs font-mono font-bold text-emerald-400">
|
||||
{Number(wusdBalance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 bg-slate-900 border border-slate-800 px-3.5 py-1.5 rounded-xl">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="font-mono text-xs font-medium text-slate-200">
|
||||
{account.slice(0, 6)}...{account.slice(-4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={connectWallet}
|
||||
className="flex items-center space-x-2 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white px-5 py-2 rounded-xl text-sm font-bold shadow-lg shadow-cyan-500/25 transition-all"
|
||||
>
|
||||
<Wallet className="w-4 h-4" />
|
||||
<span>连接钱包</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
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;
|
||||
wusdBalance: string;
|
||||
connectWallet: () => Promise<void>;
|
||||
switchNetwork: () => Promise<void>;
|
||||
refreshBalance: () => Promise<void>;
|
||||
getMarketContract: (address: string) => ethers.Contract | null;
|
||||
getCollateralContract: () => ethers.Contract | null;
|
||||
}
|
||||
|
||||
const Web3Context = createContext<Web3ContextType>({
|
||||
account: null,
|
||||
provider: null,
|
||||
signer: null,
|
||||
chainId: null,
|
||||
isCorrectNetwork: false,
|
||||
wusdBalance: '0',
|
||||
connectWallet: async () => {},
|
||||
switchNetwork: async () => {},
|
||||
refreshBalance: async () => {},
|
||||
getMarketContract: () => 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 [wusdBalance, setWusdBalance] = useState<string>('0');
|
||||
|
||||
const isCorrectNetwork = chainId === ROBINHOOD_TESTNET_CHAIN.id;
|
||||
|
||||
const refreshBalance = async () => {
|
||||
if (!account || !provider) return;
|
||||
try {
|
||||
const collateral = new ethers.Contract(
|
||||
DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
|
||||
ABIS.MockERC20,
|
||||
provider
|
||||
);
|
||||
const bal = await collateral.balanceOf(account);
|
||||
setWusdBalance(ethers.formatUnits(bal, 18));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch WUSD balance:', err);
|
||||
}
|
||||
};
|
||||
|
||||
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('Init web3 error:', err);
|
||||
}
|
||||
|
||||
(window as any).ethereum.on('accountsChanged', async (accs: string[]) => {
|
||||
if (accs.length > 0) {
|
||||
setAccount(accs[0]);
|
||||
setSigner(await browserProvider.getSigner());
|
||||
} else {
|
||||
setAccount(null);
|
||||
setSigner(null);
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).ethereum.on('chainChanged', (cId: string) => {
|
||||
setChainId(parseInt(cId, 16));
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initProvider();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (account && provider) {
|
||||
refreshBalance();
|
||||
}
|
||||
}, [account, provider, chainId]);
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
alert('请安装 MetaMask 钱包');
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMarketContract = (address: string) => {
|
||||
if (!provider) return null;
|
||||
return new ethers.Contract(address, ABIS.WTFMarketV2, signer || provider);
|
||||
};
|
||||
|
||||
const getCollateralContract = () => {
|
||||
if (!provider) return null;
|
||||
return new ethers.Contract(
|
||||
DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
|
||||
ABIS.MockERC20,
|
||||
signer || provider
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Web3Context.Provider
|
||||
value={{
|
||||
account,
|
||||
provider,
|
||||
signer,
|
||||
chainId,
|
||||
isCorrectNetwork,
|
||||
wusdBalance,
|
||||
connectWallet,
|
||||
switchNetwork,
|
||||
refreshBalance,
|
||||
getMarketContract,
|
||||
getCollateralContract,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Web3Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWeb3 = () => useContext(Web3Context);
|
||||
@@ -0,0 +1,202 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Sparkles, TrendingUp, Flame, CheckCircle, Clock, Search, ArrowUpRight } from 'lucide-react';
|
||||
|
||||
export interface MarketItem {
|
||||
id: string;
|
||||
marketAddress: string;
|
||||
title: string;
|
||||
category: string;
|
||||
tier: number;
|
||||
outcomes: { label: string; price: number; percentage: number }[];
|
||||
poolLiquidity: number;
|
||||
volume24h: number;
|
||||
endTime: string;
|
||||
status: 'ACTIVE' | 'RESOLVED' | 'FINALISED';
|
||||
}
|
||||
|
||||
const MOCK_FEATURED_MARKETS: MarketItem[] = [
|
||||
{
|
||||
id: 'm-1',
|
||||
marketAddress: '0xc0E24E152771C588B21AEB654b30B1cBAf381c1a',
|
||||
title: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||
category: 'Crypto',
|
||||
tier: 1,
|
||||
outcomes: [
|
||||
{ label: 'YES', price: 0.68, percentage: 68 },
|
||||
{ label: 'NO', price: 0.32, percentage: 32 }
|
||||
],
|
||||
poolLiquidity: 1250000,
|
||||
volume24h: 384000,
|
||||
endTime: '2026-12-31',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
id: 'm-2',
|
||||
marketAddress: '0x2Aeca49669e9E4E6CdCe46A1bc9bf136765A4f75',
|
||||
title: 'Ethereum Layer-2 Total TVL to reach $50 Billion in 2026?',
|
||||
category: 'Crypto',
|
||||
tier: 2,
|
||||
outcomes: [
|
||||
{ label: 'YES', price: 0.81, percentage: 81 },
|
||||
{ label: 'NO', price: 0.19, percentage: 19 }
|
||||
],
|
||||
poolLiquidity: 890000,
|
||||
volume24h: 192000,
|
||||
endTime: '2026-11-30',
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
{
|
||||
id: 'm-3',
|
||||
marketAddress: '0xF0E189974c413506098AFB6Bc6Bf4C6715fBf7B1',
|
||||
title: 'US Federal Reserve Interest Rate cut > 50 bps in next FOMC?',
|
||||
category: 'Macro',
|
||||
tier: 1,
|
||||
outcomes: [
|
||||
{ label: 'YES (>50bps)', price: 0.35, percentage: 35 },
|
||||
{ label: 'NO (<=50bps)', price: 0.65, percentage: 65 }
|
||||
],
|
||||
poolLiquidity: 620000,
|
||||
volume24h: 98000,
|
||||
endTime: '2026-09-30',
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
];
|
||||
|
||||
interface MarketExploreProps {
|
||||
onSelectMarket: (market: MarketItem) => void;
|
||||
}
|
||||
|
||||
export const MarketExplore: React.FC<MarketExploreProps> = ({ onSelectMarket }) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const categories = ['All', 'Crypto', 'Macro', 'Politics', 'Sports', 'Tech'];
|
||||
|
||||
const filteredMarkets = MOCK_FEATURED_MARKETS.filter((m) => {
|
||||
const matchCategory = selectedCategory === 'All' || m.category === selectedCategory;
|
||||
const matchSearch = m.title.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchCategory && matchSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Hero Banner */}
|
||||
<div className="relative rounded-3xl overflow-hidden border border-slate-800 bg-gradient-to-r from-slate-950 via-slate-900 to-indigo-950/40 p-8 sm:p-12 shadow-2xl">
|
||||
<div className="absolute right-0 top-0 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div className="max-w-2xl space-y-4 relative z-10">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 text-xs font-semibold">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>Power LDA 幂律连续流动性引擎</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-black text-white tracking-tight leading-tight">
|
||||
全球最具流动性的 <br />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 via-indigo-300 to-purple-400">
|
||||
去中心化预测交易市场
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm sm:text-base leading-relaxed">
|
||||
零滑点聚合撮合,支持多选项连续拍卖与极速链上结算。抢先参与热门预测,兑现你的认知价值。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter & Search Bar */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Category Tabs */}
|
||||
<div className="flex items-center space-x-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
selectedCategory === cat
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative w-full md:w-72">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3.5 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索预测事件 / 资产..."
|
||||
className="w-full bg-slate-900/90 border border-slate-800 rounded-xl pl-9 pr-4 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Market Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredMarkets.map((market) => (
|
||||
<div
|
||||
key={market.id}
|
||||
onClick={() => onSelectMarket(market)}
|
||||
className="group bg-[#0f111d] hover:bg-[#141726] border border-slate-800/80 hover:border-cyan-500/40 rounded-2xl p-5 cursor-pointer transition-all duration-200 hover:shadow-xl hover:shadow-cyan-950/20 flex flex-col justify-between"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Category & Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-bold px-2.5 py-0.5 rounded-md bg-slate-800 text-cyan-400 border border-slate-700">
|
||||
{market.category}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1.5 text-[11px] text-slate-500">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>{market.endTime} 截止</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-bold text-base text-slate-100 group-hover:text-white line-clamp-2 leading-snug">
|
||||
{market.title}
|
||||
</h3>
|
||||
|
||||
{/* Outcomes Bars */}
|
||||
<div className="space-y-2 pt-1">
|
||||
{market.outcomes.map((outcome, idx) => (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="flex justify-between text-xs font-semibold">
|
||||
<span className={idx === 0 ? 'text-emerald-400' : 'text-rose-400'}>
|
||||
{outcome.label}
|
||||
</span>
|
||||
<span className="text-slate-300 font-mono">${outcome.price.toFixed(2)} ({outcome.percentage}%)</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800/60">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
idx === 0 ? 'bg-gradient-to-r from-emerald-500 to-teal-400' : 'bg-gradient-to-r from-rose-500 to-pink-500'
|
||||
}`}
|
||||
style={{ width: `${outcome.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer / Stats */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
|
||||
<div>
|
||||
<span className="block text-slate-500">24H 交易量</span>
|
||||
<span className="font-mono font-bold text-slate-200">${market.volume24h.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="block text-slate-500">流动性深度</span>
|
||||
<span className="font-mono font-bold text-slate-200">${market.poolLiquidity.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="p-2 rounded-xl bg-slate-800/50 group-hover:bg-cyan-500/20 group-hover:text-cyan-400 transition-colors">
|
||||
<ArrowUpRight className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { PieChart, Clock, ArrowUpRight, ArrowDownLeft, ShieldCheck, History } from 'lucide-react';
|
||||
|
||||
export const PortfolioView: React.FC = () => {
|
||||
const { account, wusdBalance } = useWeb3();
|
||||
|
||||
const MOCK_POSITIONS = [
|
||||
{
|
||||
id: 'pos-1',
|
||||
marketTitle: 'Will Bitcoin break above $120,000 before end of Q4 2026?',
|
||||
outcome: 'YES',
|
||||
shares: 1470.5,
|
||||
avgPrice: 0.68,
|
||||
currentPrice: 0.72,
|
||||
currentValue: 1058.76,
|
||||
pnl: 58.76,
|
||||
pnlPct: 5.88
|
||||
},
|
||||
{
|
||||
id: 'pos-2',
|
||||
marketTitle: 'Ethereum Layer-2 Total TVL to reach $50 Billion in 2026?',
|
||||
outcome: 'YES',
|
||||
shares: 617.28,
|
||||
avgPrice: 0.81,
|
||||
currentPrice: 0.85,
|
||||
currentValue: 524.68,
|
||||
pnl: 24.68,
|
||||
pnlPct: 4.93
|
||||
}
|
||||
];
|
||||
|
||||
const MOCK_LEDGER_ENTRIES = [
|
||||
{
|
||||
id: 'led-1',
|
||||
type: 'TRADE_BUY',
|
||||
desc: '买入 BTC > $120K (YES 1470.5 份)',
|
||||
amount: -1000.0,
|
||||
fee: 6.0,
|
||||
time: '2026-08-31 16:32:10'
|
||||
},
|
||||
{
|
||||
id: 'led-2',
|
||||
type: 'TRADE_BUY',
|
||||
desc: '买入 ETH L2 TVL > $50B (YES 617.28 份)',
|
||||
amount: -500.0,
|
||||
fee: 3.0,
|
||||
time: '2026-08-31 15:45:00'
|
||||
},
|
||||
{
|
||||
id: 'led-3',
|
||||
type: 'DEPOSIT',
|
||||
desc: '链上充值入账 WUSD',
|
||||
amount: 100000.0,
|
||||
fee: 0.0,
|
||||
time: '2026-08-31 14:10:22'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Portfolio Top Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">总持仓市值 (Portfolio Value)</span>
|
||||
<span className="text-2xl font-black font-mono text-white mt-1 block">$1,583.44 WUSD</span>
|
||||
<div className="flex items-center space-x-1.5 text-xs text-emerald-400 font-bold mt-2">
|
||||
<span>+$83.44 (+5.56%)</span>
|
||||
<span className="text-slate-500 font-normal">未实现盈亏</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">可用保证金余额 (Available Balance)</span>
|
||||
<span className="text-2xl font-black font-mono text-cyan-400 mt-1 block">
|
||||
{Number(wusdBalance).toLocaleString(undefined, { maximumFractionDigits: 2 })} WUSD
|
||||
</span>
|
||||
<div className="text-xs text-slate-500 mt-2">随时可用于下注买入或提现</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl">
|
||||
<span className="text-xs text-slate-400 block font-medium">活跃预测头寸 (Active Positions)</span>
|
||||
<span className="text-2xl font-black font-mono text-slate-200 mt-1 block">2 个市场</span>
|
||||
<div className="text-xs text-slate-500 mt-2">持仓均基于不可变智能合约保护</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions Table */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center space-x-2 border-b border-slate-800/80 pb-4">
|
||||
<PieChart className="w-5 h-5 text-cyan-400" />
|
||||
<h2 className="font-bold text-base text-white">当前持仓明细 (Active Positions)</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-500 uppercase">
|
||||
<th className="pb-3 font-semibold">预测市场</th>
|
||||
<th className="pb-3 font-semibold">预测选项</th>
|
||||
<th className="pb-3 font-semibold text-right">持有份额</th>
|
||||
<th className="pb-3 font-semibold text-right">开仓均价</th>
|
||||
<th className="pb-3 font-semibold text-right">当前市价</th>
|
||||
<th className="pb-3 font-semibold text-right">当前估值</th>
|
||||
<th className="pb-3 font-semibold text-right">未实现盈亏</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{MOCK_POSITIONS.map((pos) => (
|
||||
<tr key={pos.id} className="hover:bg-slate-900/40">
|
||||
<td className="py-4 font-semibold text-slate-200 max-w-xs truncate">{pos.marketTitle}</td>
|
||||
<td className="py-4">
|
||||
<span className="px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50 font-bold">
|
||||
{pos.outcome}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 text-right font-mono text-slate-300">{pos.shares.toLocaleString()}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-400">${pos.avgPrice}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-200 font-bold">${pos.currentPrice}</td>
|
||||
<td className="py-4 text-right font-mono text-slate-100 font-bold">${pos.currentValue.toFixed(2)}</td>
|
||||
<td className="py-4 text-right font-mono font-bold text-emerald-400">
|
||||
+${pos.pnl.toFixed(2)} (+{pos.pnlPct}%)
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Immutable Ledger History Table */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center space-x-2 border-b border-slate-800/80 pb-4">
|
||||
<History className="w-5 h-5 text-indigo-400" />
|
||||
<div>
|
||||
<h2 className="font-bold text-base text-white">不可变复式记账流水 (Immutable Ledger)</h2>
|
||||
<p className="text-xs text-slate-500">法定链上/链下资金变动流水,防篡改逐笔审计</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-800/60">
|
||||
{MOCK_LEDGER_ENTRIES.map((entry) => (
|
||||
<div key={entry.id} className="py-3.5 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`p-2 rounded-xl border ${
|
||||
entry.type === 'DEPOSIT'
|
||||
? 'bg-emerald-950/40 border-emerald-800/50 text-emerald-400'
|
||||
: 'bg-indigo-950/40 border-indigo-800/50 text-indigo-400'
|
||||
}`}
|
||||
>
|
||||
{entry.type === 'DEPOSIT' ? <ArrowDownLeft className="w-4 h-4" /> : <ArrowUpRight className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-200">{entry.desc}</div>
|
||||
<div className="text-slate-500 text-[11px] mt-0.5">{entry.time} • 手续费: ${entry.fee}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right font-mono font-bold">
|
||||
<span className={entry.amount >= 0 ? 'text-emerald-400' : 'text-slate-200'}>
|
||||
{entry.amount >= 0 ? `+${entry.amount.toLocaleString()}` : `${entry.amount.toLocaleString()}`} WUSD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,304 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useWeb3 } from '../../context/Web3Context';
|
||||
import { MarketItem } from '../explore/MarketExplore';
|
||||
import { ethers } from 'ethers';
|
||||
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface TradingTerminalProps {
|
||||
market: MarketItem;
|
||||
}
|
||||
|
||||
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
||||
const { account, signer, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3();
|
||||
|
||||
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
|
||||
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
||||
const [amount, setAmount] = useState<string>('100');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const activeOutcome = market.outcomes[selectedOutcomeIndex];
|
||||
const estShares = amount && Number(amount) > 0 ? (Number(amount) / activeOutcome.price).toFixed(2) : '0';
|
||||
const estPayout = (Number(estShares) * 1.0).toFixed(2);
|
||||
const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0';
|
||||
|
||||
const handleTrade = 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);
|
||||
|
||||
const collateral = getCollateralContract();
|
||||
if (!collateral) throw new Error('Collateral contract not loaded');
|
||||
|
||||
const amountWei = ethers.parseUnits(amount, 18);
|
||||
|
||||
// Step 1: Approve Collateral
|
||||
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权扣减 WUSD 保证金...' });
|
||||
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
|
||||
await appTx.wait();
|
||||
|
||||
// Step 2: In web2-like demo / testnet, complete trade
|
||||
setStatusMsg({ type: 'success', text: `🎉 订单已撮合成交!已以 $${activeOutcome.price} 买入 ${estShares} 份 ${activeOutcome.label} 预测代币。` });
|
||||
refreshBalance();
|
||||
} catch (err: any) {
|
||||
console.error('Trade error:', err);
|
||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left 2 Cols: Market Title, Chart, Curve Status */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Header Summary */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold px-2.5 py-1 rounded bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
|
||||
{market.category} • Tier {market.tier}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
|
||||
<span>结算截止: {market.endTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl sm:text-2xl font-black text-white leading-snug">
|
||||
{market.title}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">24H 交易量</span>
|
||||
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">流动性资金池</span>
|
||||
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">联合曲线状态</span>
|
||||
<span className="text-sm font-bold text-emerald-400">Power LDA (活跃)</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
||||
<span className="text-[11px] text-slate-500 block">协议手续费</span>
|
||||
<span className="text-sm font-bold font-mono text-slate-300">0.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* High-frequency Chart Simulation */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-slate-800/80 pb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Activity className="w-5 h-5 text-cyan-400" />
|
||||
<h3 className="font-bold text-sm text-white">实时胜率走势 & 概率曲线 (Real-Time Price & Probability)</h3>
|
||||
</div>
|
||||
<div className="flex space-x-1.5 text-xs font-mono">
|
||||
{['1H', '6H', '24H', '7D', 'ALL'].map((tf, i) => (
|
||||
<button
|
||||
key={tf}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
i === 2 ? 'bg-cyan-500/20 text-cyan-400 font-bold' : 'text-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SVG Chart Graphic */}
|
||||
<div className="h-64 w-full flex flex-col justify-between relative pt-4">
|
||||
<svg className="w-full h-48 overflow-visible" viewBox="0 0 500 150">
|
||||
<defs>
|
||||
<linearGradient id="curveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="#06b6d4" stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor="#06b6d4" stopOpacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Grid Lines */}
|
||||
<line x1="0" y1="30" x2="500" y2="30" stroke="#1f2438" strokeDasharray="4" />
|
||||
<line x1="0" y1="75" x2="500" y2="75" stroke="#1f2438" strokeDasharray="4" />
|
||||
<line x1="0" y1="120" x2="500" y2="120" stroke="#1f2438" strokeDasharray="4" />
|
||||
|
||||
{/* Area Fill */}
|
||||
<path
|
||||
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35 L 500 150 L 0 150 Z"
|
||||
fill="url(#curveGradient)"
|
||||
/>
|
||||
{/* Main Stroke */}
|
||||
<path
|
||||
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35"
|
||||
fill="none"
|
||||
stroke="#06b6d4"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<circle cx="500" cy="35" r="4" fill="#38bdf8" className="animate-ping" />
|
||||
<circle cx="500" cy="35" r="4" fill="#38bdf8" />
|
||||
</svg>
|
||||
|
||||
<div className="flex justify-between text-[11px] font-mono text-slate-500 pt-2">
|
||||
<span>00:00</span>
|
||||
<span>06:00</span>
|
||||
<span>12:00</span>
|
||||
<span>18:00</span>
|
||||
<span>LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Col: Instant Order Placement Panel */}
|
||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6 flex flex-col justify-between">
|
||||
<form onSubmit={handleTrade} className="space-y-5">
|
||||
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
|
||||
<h2 className="font-bold text-base text-white flex items-center space-x-2">
|
||||
<TrendingUp className="w-5 h-5 text-cyan-400" />
|
||||
<span>快速交易 (Fast Order)</span>
|
||||
</h2>
|
||||
|
||||
{/* Buy / Sell Tab */}
|
||||
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTradeSide('BUY')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
tradeSide === 'BUY' ? 'bg-cyan-600 text-white' : 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
买入 (Buy)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTradeSide('SELL')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
tradeSide === 'SELL' ? 'bg-rose-600 text-white' : 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
卖出 (Sell)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outcome Selectors */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-slate-400">选择预测结果 (Outcome)</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{market.outcomes.map((outcome, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => setSelectedOutcomeIndex(idx)}
|
||||
className={`p-3.5 rounded-xl border text-left transition-all ${
|
||||
selectedOutcomeIndex === idx
|
||||
? 'bg-cyan-950/40 border-cyan-500 text-white ring-1 ring-cyan-500'
|
||||
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-extrabold text-sm">{outcome.label}</div>
|
||||
<div className="text-xs font-mono mt-1 text-cyan-400 font-bold">${outcome.price} ({outcome.percentage}%)</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="font-semibold text-slate-400">交易金额 (Amount WUSD)</span>
|
||||
<span className="text-slate-500 font-mono">余额: {Number(wusdBalance).toFixed(2)} WUSD</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="100"
|
||||
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex space-x-1">
|
||||
{['50', '100', '500', 'MAX'].map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
|
||||
className="px-2 py-0.5 text-[10px] font-bold rounded bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Estimations */}
|
||||
<div className="bg-slate-950 border border-slate-800/80 rounded-xl p-3.5 space-y-2 text-xs">
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>预估获买份额 (Est. Shares)</span>
|
||||
<span className="font-mono font-bold text-slate-200">{estShares} 份</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>若胜出最终兑付 (Max Payout)</span>
|
||||
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>预期收益率 (Est. ROI)</span>
|
||||
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !amount || Number(amount) <= 0}
|
||||
className="w-full py-3.5 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"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>正在上链成交中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<span>立即买入 {activeOutcome.label}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Status Message */}
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-3.5 rounded-xl border flex items-start space-x-2.5 text-xs ${
|
||||
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-4 h-4 flex-shrink-0 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 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,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: []
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Generated
+34
@@ -75,15 +75,24 @@ importers:
|
||||
'@wtfx/api-client':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/api-client
|
||||
'@wtfx/contracts':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/contracts
|
||||
'@wtfx/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
'@wtfx/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
ethers:
|
||||
specifier: ^6.13.0
|
||||
version: 6.17.0
|
||||
lucide-react:
|
||||
specifier: ^0.395.0
|
||||
version: 0.395.0(react@18.3.1)
|
||||
next:
|
||||
specifier: ^14.2.4
|
||||
version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -93,6 +102,31 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
tailwind-merge:
|
||||
specifier: ^2.3.0
|
||||
version: 2.6.1
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.14.2
|
||||
version: 20.19.43
|
||||
'@types/react':
|
||||
specifier: ^18.3.3
|
||||
version: 18.3.31
|
||||
'@types/react-dom':
|
||||
specifier: ^18.3.0
|
||||
version: 18.3.7(@types/react@18.3.31)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.5.4(postcss@8.5.26)
|
||||
postcss:
|
||||
specifier: ^8.4.38
|
||||
version: 8.5.26
|
||||
tailwindcss:
|
||||
specifier: ^3.4.4
|
||||
version: 3.4.19
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.9.3
|
||||
|
||||
packages/api-client:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user