This commit is contained in:
2026-01-02 10:43:20 -06:00
commit 14d9af3036
112 changed files with 14274 additions and 0 deletions

View File

@ -0,0 +1,70 @@
import { useState, FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/store'
import { Button } from '@/components/common/Button'
export const LoginForm = () => {
const navigate = useNavigate()
const { login } = useAuthStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setError('')
setIsLoading(true)
try {
await login(email, password)
navigate('/dashboard')
} catch (err: any) {
setError(err.response?.data?.detail || 'Login failed. Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</form>
)
}

View File

@ -0,0 +1,101 @@
import { useState, FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/store'
import { Button } from '@/components/common/Button'
export const RegisterForm = () => {
const navigate = useNavigate()
const { register } = useAuthStore()
const [email, setEmail] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [displayName, setDisplayName] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setError('')
setIsLoading(true)
try {
await register(email, username, password, displayName || undefined)
navigate('/dashboard')
} catch (err: any) {
setError(err.response?.data?.detail || 'Registration failed. Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label htmlFor="displayName" className="block text-sm font-medium text-gray-700 mb-1">
Display Name (optional)
</label>
<input
id="displayName"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Registering...' : 'Register'}
</Button>
</form>
)
}

View File

@ -0,0 +1,66 @@
import { Link } from 'react-router-dom'
import type { Bet } from '@/types'
import { Card } from '@/components/common/Card'
import { formatCurrency, formatRelativeTime } from '@/utils/formatters'
import { BET_CATEGORIES, BET_STATUS_COLORS } from '@/utils/constants'
import { Calendar, DollarSign, TrendingUp } from 'lucide-react'
import { BlockchainBadgeCompact } from '@/blockchain/components/BlockchainBadge'
interface BetCardProps {
bet: Bet
}
export const BetCard = ({ bet }: BetCardProps) => {
return (
<Link to={`/bets/${bet.id}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="space-y-3">
<div className="flex items-start justify-between">
<h3 className="text-lg font-semibold text-gray-900">{bet.title}</h3>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${BET_STATUS_COLORS[bet.status]}`}>
{bet.status}
</span>
{bet.blockchain_tx_hash && (
<BlockchainBadgeCompact
status="confirmed"
txHash={bet.blockchain_tx_hash}
/>
)}
</div>
</div>
<p className="text-sm text-gray-600 line-clamp-2">{bet.description}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Calendar size={16} />
<span>{bet.event_name}</span>
</div>
<span className="px-2 py-0.5 bg-gray-100 rounded text-xs">
{BET_CATEGORIES[bet.category]}
</span>
</div>
<div className="pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-primary font-semibold">
<DollarSign size={18} />
{formatCurrency(bet.stake_amount)}
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<TrendingUp size={16} />
<span>{bet.creator_odds}x / {bet.opponent_odds}x</span>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Created by {bet.creator.display_name || bet.creator.username}</span>
<span className="text-gray-500">{formatRelativeTime(bet.created_at)}</span>
</div>
</div>
</div>
</Card>
</Link>
)
}

View File

@ -0,0 +1,24 @@
import type { Bet } from '@/types'
import { BetCard } from './BetCard'
interface BetListProps {
bets: Bet[]
}
export const BetList = ({ bets }: BetListProps) => {
if (bets.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
No bets found
</div>
)
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{bets.map((bet) => (
<BetCard key={bet.id} bet={bet} />
))}
</div>
)
}

View File

@ -0,0 +1,272 @@
import { useState, FormEvent } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { betsApi } from '@/api/bets'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/common/Button'
import type { BetCategory } from '@/types'
import { BET_CATEGORIES } from '@/utils/constants'
import { useBlockchainBet } from '@/blockchain/hooks/useBlockchainBet'
import { useGasEstimate } from '@/blockchain/hooks/useGasEstimate'
import { TransactionModal } from '@/blockchain/components/TransactionModal'
interface CreateBetModalProps {
isOpen: boolean
onClose: () => void
}
export const CreateBetModal = ({ isOpen, onClose }: CreateBetModalProps) => {
const queryClient = useQueryClient()
const [formData, setFormData] = useState({
title: '',
description: '',
category: 'sports' as BetCategory,
event_name: '',
creator_position: '',
opponent_position: '',
stake_amount: '',
creator_odds: '1',
opponent_odds: '1',
})
// Blockchain integration
const { createBet: createBlockchainBet, txStatus, txHash } = useBlockchainBet()
const gasEstimate = useGasEstimate(
'create_bet',
{ stakeAmount: parseFloat(formData.stake_amount) || 0 }
)
const createMutation = useMutation({
mutationFn: betsApi.createBet,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bets'] })
onClose()
setFormData({
title: '',
description: '',
category: 'sports',
event_name: '',
creator_position: '',
opponent_position: '',
stake_amount: '',
creator_odds: '1',
opponent_odds: '1',
})
},
})
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
const betData = {
...formData,
stake_amount: parseFloat(formData.stake_amount),
creator_odds: parseFloat(formData.creator_odds),
opponent_odds: parseFloat(formData.opponent_odds),
}
// Create bet with blockchain integration
// This will: 1) Create in backend, 2) Sign transaction, 3) Update with blockchain ID
try {
await createBlockchainBet(betData)
queryClient.invalidateQueries({ queryKey: ['bets'] })
onClose()
setFormData({
title: '',
description: '',
category: 'sports',
event_name: '',
creator_position: '',
opponent_position: '',
stake_amount: '',
creator_odds: '1',
opponent_odds: '1',
})
} catch (error) {
console.error('Failed to create bet:', error)
}
}
return (
<Modal isOpen={isOpen} onClose={onClose} title="Create New Bet">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
maxLength={200}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
required
rows={3}
maxLength={2000}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as BetCategory })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
>
{Object.entries(BET_CATEGORIES).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Event Name
</label>
<input
type="text"
value={formData.event_name}
onChange={(e) => setFormData({ ...formData, event_name: e.target.value })}
required
maxLength={200}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Position
</label>
<input
type="text"
value={formData.creator_position}
onChange={(e) => setFormData({ ...formData, creator_position: e.target.value })}
required
maxLength={500}
placeholder="What are you betting on?"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Opponent Position
</label>
<input
type="text"
value={formData.opponent_position}
onChange={(e) => setFormData({ ...formData, opponent_position: e.target.value })}
required
maxLength={500}
placeholder="What is the opposing position?"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Stake Amount ($)
</label>
<input
type="number"
value={formData.stake_amount}
onChange={(e) => setFormData({ ...formData, stake_amount: e.target.value })}
required
min="0.01"
max="10000"
step="0.01"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Odds
</label>
<input
type="number"
value={formData.creator_odds}
onChange={(e) => setFormData({ ...formData, creator_odds: e.target.value })}
min="0.01"
step="0.1"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Opponent Odds
</label>
<input
type="number"
value={formData.opponent_odds}
onChange={(e) => setFormData({ ...formData, opponent_odds: e.target.value })}
min="0.01"
step="0.1"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
{/* Gas Estimate */}
{formData.stake_amount && parseFloat(formData.stake_amount) > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-xs font-medium text-blue-800 mb-1">Estimated Gas Cost</div>
{gasEstimate.isLoading ? (
<div className="text-sm text-blue-600">Calculating...</div>
) : gasEstimate.error ? (
<div className="text-sm text-red-600">{gasEstimate.error}</div>
) : (
<div className="flex items-center justify-between text-sm">
<span className="text-blue-900">{gasEstimate.costEth} ETH</span>
<span className="text-blue-700"> ${gasEstimate.costUsd}</span>
</div>
)}
</div>
)}
<div className="flex gap-4 pt-4">
<Button type="button" variant="secondary" onClick={onClose} className="flex-1">
Cancel
</Button>
<Button type="submit" className="flex-1" disabled={txStatus === 'pending' || txStatus === 'confirming'}>
{txStatus === 'pending' || txStatus === 'confirming' ? 'Processing...' : 'Create Bet'}
</Button>
</div>
</form>
{/* Transaction Status Modal */}
<TransactionModal
isOpen={txStatus !== 'idle'}
status={txStatus}
txHash={txHash}
message={
txStatus === 'success'
? 'Your bet has been created on the blockchain!'
: txStatus === 'error'
? 'Failed to create bet on blockchain'
: undefined
}
onClose={() => {}}
autoCloseOnSuccess={true}
autoCloseDelay={2000}
/>
</Modal>
)
}

View File

@ -0,0 +1,39 @@
import { ButtonHTMLAttributes, ReactNode } from 'react'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'success'
size?: 'sm' | 'md' | 'lg'
children: ReactNode
}
export const Button = ({
variant = 'primary',
size = 'md',
className = '',
children,
...props
}: ButtonProps) => {
const baseClasses = 'font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
const variants = {
primary: 'bg-primary text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-error text-white hover:bg-red-600',
success: 'bg-success text-white hover:bg-green-600',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button
className={`${baseClasses} ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
>
{children}
</button>
)
}

View File

@ -0,0 +1,14 @@
import { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
}
export const Card = ({ children, className = '' }: CardProps) => {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
{children}
</div>
)
}

View File

@ -0,0 +1,7 @@
export const Loading = () => {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
)
}

View File

@ -0,0 +1,37 @@
import { ReactNode } from 'react'
import { X } from 'lucide-react'
interface ModalProps {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
}
export const Modal = ({ isOpen, onClose, title, children }: ModalProps) => {
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-screen items-center justify-center p-4">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={onClose} />
<div className="relative bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-2xl font-bold">{title}</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X size={24} />
</button>
</div>
<div className="p-6">
{children}
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,72 @@
import { Link } from 'react-router-dom'
import { useAuthStore } from '@/store'
import { Wallet, LogOut, User } from 'lucide-react'
import { useWeb3Wallet } from '@/blockchain/hooks/useWeb3Wallet'
export const Header = () => {
const { user, logout } = useAuthStore()
const { walletAddress, isConnected, connectWallet, disconnectWallet } = useWeb3Wallet()
return (
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<Link to="/" className="flex items-center">
<h1 className="text-2xl font-bold text-primary">H2H</h1>
</Link>
{user && (
<nav className="flex items-center gap-6">
<Link to="/dashboard" className="text-gray-700 hover:text-primary transition-colors">
Dashboard
</Link>
<Link to="/marketplace" className="text-gray-700 hover:text-primary transition-colors">
Marketplace
</Link>
<Link to="/my-bets" className="text-gray-700 hover:text-primary transition-colors">
My Bets
</Link>
<Link to="/wallet" className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
<Wallet size={18} />
Wallet
</Link>
{/* Web3 Wallet Connection */}
{isConnected ? (
<button
onClick={disconnectWallet}
className="flex items-center gap-2 px-3 py-1.5 bg-green-100 text-green-800 rounded-lg hover:bg-green-200 transition-colors text-sm font-medium"
>
<span></span>
<span>{walletAddress?.substring(0, 6)}...{walletAddress?.substring(38)}</span>
</button>
) : (
<button
onClick={connectWallet}
className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-800 rounded-lg hover:bg-blue-200 transition-colors text-sm font-medium"
>
<span></span>
<span>Connect Wallet</span>
</button>
)}
<div className="flex items-center gap-4 pl-6 border-l">
<Link to="/profile" className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
<User size={18} />
{user.display_name || user.username}
</Link>
<button
onClick={logout}
className="flex items-center gap-2 text-gray-700 hover:text-error transition-colors"
>
<LogOut size={18} />
Logout
</button>
</div>
</nav>
)}
</div>
</div>
</header>
)
}

View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react'
import { Header } from './Header'
interface LayoutProps {
children: ReactNode
}
export const Layout = ({ children }: LayoutProps) => {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{children}
</main>
</div>
)
}

View File

@ -0,0 +1,65 @@
import { useState, FormEvent } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { walletApi } from '@/api/wallet'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/common/Button'
interface DepositModalProps {
isOpen: boolean
onClose: () => void
}
export const DepositModal = ({ isOpen, onClose }: DepositModalProps) => {
const queryClient = useQueryClient()
const [amount, setAmount] = useState('')
const depositMutation = useMutation({
mutationFn: walletApi.deposit,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet'] })
queryClient.invalidateQueries({ queryKey: ['transactions'] })
setAmount('')
onClose()
},
})
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
depositMutation.mutate(amount)
}
return (
<Modal isOpen={isOpen} onClose={onClose} title="Deposit Funds">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Amount ($)
</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
min="0.01"
max="10000"
step="0.01"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Enter amount"
/>
<p className="mt-1 text-sm text-gray-500">
This is a simulated deposit for MVP testing purposes.
</p>
</div>
<div className="flex gap-4">
<Button type="button" variant="secondary" onClick={onClose} className="flex-1">
Cancel
</Button>
<Button type="submit" className="flex-1" disabled={depositMutation.isPending}>
{depositMutation.isPending ? 'Processing...' : 'Deposit'}
</Button>
</div>
</form>
</Modal>
)
}

View File

@ -0,0 +1,54 @@
import { useQuery } from '@tanstack/react-query'
import { walletApi } from '@/api/wallet'
import { Card } from '@/components/common/Card'
import { formatCurrency, formatDateTime } from '@/utils/formatters'
import { ArrowUpRight, ArrowDownRight } from 'lucide-react'
import { Loading } from '@/components/common/Loading'
export const TransactionHistory = () => {
const { data: transactions, isLoading } = useQuery({
queryKey: ['transactions'],
queryFn: () => walletApi.getTransactions(),
})
if (isLoading) return <Loading />
return (
<Card>
<h2 className="text-xl font-bold mb-4">Transaction History</h2>
<div className="space-y-3">
{transactions && transactions.length > 0 ? (
transactions.map((tx) => (
<div key={tx.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-full ${
parseFloat(tx.amount) >= 0 ? 'bg-success/10 text-success' : 'bg-error/10 text-error'
}`}>
{parseFloat(tx.amount) >= 0 ? <ArrowDownRight size={20} /> : <ArrowUpRight size={20} />}
</div>
<div>
<p className="font-medium">{tx.description}</p>
<p className="text-sm text-gray-500">{formatDateTime(tx.created_at)}</p>
</div>
</div>
<div className="text-right">
<p className={`font-semibold ${
parseFloat(tx.amount) >= 0 ? 'text-success' : 'text-error'
}`}>
{parseFloat(tx.amount) >= 0 ? '+' : ''}{formatCurrency(tx.amount)}
</p>
<p className="text-sm text-gray-500">
Balance: {formatCurrency(tx.balance_after)}
</p>
</div>
</div>
))
) : (
<p className="text-center text-gray-500 py-8">No transactions yet</p>
)}
</div>
</Card>
)
}

View File

@ -0,0 +1,94 @@
import { useQuery } from '@tanstack/react-query'
import { walletApi } from '@/api/wallet'
import { Card } from '@/components/common/Card'
import { formatCurrency } from '@/utils/formatters'
import { Wallet, Lock } from 'lucide-react'
import { Loading } from '@/components/common/Loading'
import { useWeb3Wallet } from '@/blockchain/hooks/useWeb3Wallet'
import { BlockchainBadge } from '@/blockchain/components/BlockchainBadge'
export const WalletBalance = () => {
const { data: wallet, isLoading } = useQuery({
queryKey: ['wallet'],
queryFn: walletApi.getWallet,
})
// Blockchain integration
const { walletAddress, isConnected, walletBalance } = useWeb3Wallet()
if (isLoading) return <Loading />
if (!wallet) return null
const totalFunds = parseFloat(wallet.balance) + parseFloat(wallet.escrow)
const onChainEscrow = wallet.blockchain_escrow || 0 // Placeholder for on-chain escrow amount
return (
<Card>
<div className="space-y-4">
<h2 className="text-xl font-bold flex items-center gap-2">
<Wallet size={24} />
Wallet Balance
</h2>
<div className="space-y-3">
<div className="flex justify-between items-center p-4 bg-primary/10 rounded-lg">
<span className="text-gray-700 font-medium">Available Balance</span>
<span className="text-2xl font-bold text-primary">{formatCurrency(wallet.balance)}</span>
</div>
<div className="flex justify-between items-center p-4 bg-warning/10 rounded-lg">
<span className="text-gray-700 font-medium flex items-center gap-2">
<Lock size={18} />
Locked in Escrow
</span>
<span className="text-xl font-semibold text-warning">{formatCurrency(wallet.escrow)}</span>
</div>
<div className="flex justify-between items-center p-4 bg-gray-100 rounded-lg">
<span className="text-gray-700 font-medium">Total Funds</span>
<span className="text-xl font-semibold text-gray-900">{formatCurrency(totalFunds)}</span>
</div>
</div>
{/* On-Chain Escrow Section */}
{isConnected && (
<div className="mt-6 pt-6 border-t">
<div className="flex items-center gap-2 mb-3">
<h3 className="text-lg font-semibold text-gray-900">On-Chain Escrow</h3>
<BlockchainBadge status="confirmed" variant="compact" />
</div>
<div className="space-y-3">
<div className="flex justify-between items-center p-4 bg-green-50 border border-green-200 rounded-lg">
<span className="text-gray-700 font-medium">Locked in Smart Contract</span>
<div className="text-right">
<div className="text-lg font-semibold text-green-800">
{onChainEscrow} ETH
</div>
<div className="text-sm text-green-600">
{formatCurrency(onChainEscrow * 2000)}
</div>
</div>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span className="text-sm text-gray-600">Wallet Balance</span>
<span className="text-sm font-medium text-gray-900">{walletBalance} ETH</span>
</div>
{walletAddress && (
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<span className="text-sm text-gray-600">Wallet Address</span>
<code className="text-xs text-gray-700 bg-gray-200 px-2 py-1 rounded font-mono">
{walletAddress.substring(0, 6)}...{walletAddress.substring(38)}
</code>
</div>
)}
</div>
</div>
)}
</div>
</Card>
)
}