mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 22:57:55 +00:00
feat: complete i18n support for all components (6 languages)
Add full internationalization across 127+ components and pages. 790+ translation keys in en, tr, kmr, ckb, ar, fa locales. Remove duplicate keys and delete unused .json locale files.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -23,6 +24,7 @@ interface OfferWithReputation extends P2PFiatOffer {
|
||||
}
|
||||
|
||||
export function AdList({ type, filters }: AdListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [offers, setOffers] = useState<OfferWithReputation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -184,7 +186,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-400">
|
||||
{type === 'my-ads' ? 'You have no active offers' : 'No offers available'}
|
||||
{type === 'my-ads' ? t('p2pAd.noActiveOffers') : t('p2pAd.noOffers')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -212,16 +214,16 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
<MerchantTierBadge tier={offer.merchant_tier} size="sm" />
|
||||
)}
|
||||
{offer.seller_reputation?.verified_merchant && (
|
||||
<Shield className="w-4 h-4 text-blue-400" title="Verified Merchant" />
|
||||
<Shield className="w-4 h-4 text-blue-400" title={t('p2p.verifiedMerchant')} />
|
||||
)}
|
||||
{offer.seller_reputation?.fast_trader && (
|
||||
<Zap className="w-4 h-4 text-yellow-400" title="Fast Trader" />
|
||||
<Zap className="w-4 h-4 text-yellow-400" title={t('p2p.fastTrader')} />
|
||||
)}
|
||||
</div>
|
||||
{offer.seller_reputation && (
|
||||
<p className="text-sm text-gray-400">
|
||||
{offer.seller_reputation.completed_trades} trades • {' '}
|
||||
{((offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100).toFixed(0)}% completion
|
||||
{t('p2p.trades', { count: offer.seller_reputation.completed_trades })} • {' '}
|
||||
{t('p2p.completion', { percent: ((offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100).toFixed(0) })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -229,7 +231,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
|
||||
{/* Price */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Price</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.price')}</p>
|
||||
<p className="text-xl font-bold text-green-400">
|
||||
{offer.price_per_unit.toFixed(2)} {offer.fiat_currency}
|
||||
</p>
|
||||
@@ -237,25 +239,25 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
|
||||
{/* Available */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Available</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.available')}</p>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{offer.remaining_amount} {offer.token}
|
||||
</p>
|
||||
{offer.min_order_amount && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Min: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minLimit', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Payment</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.payment')}</p>
|
||||
<Badge variant="outline" className="mt-1">
|
||||
{offer.payment_method_name || 'N/A'}
|
||||
{offer.payment_method_name || t('p2p.na')}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{offer.time_limit_minutes} min limit
|
||||
{t('p2p.timeLimit', { minutes: offer.time_limit_minutes })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -263,16 +265,16 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{offer.seller_id === user?.id && type !== 'my-ads' && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-500/10 text-blue-400 border-blue-500/30">
|
||||
Your Ad
|
||||
{t('p2pAd.yourAd')}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setSelectedOffer(offer)}
|
||||
disabled={type === 'my-ads' || offer.seller_id === user?.id}
|
||||
className="w-full md:w-auto"
|
||||
title={offer.seller_id === user?.id ? "You can't trade with your own ad" : ''}
|
||||
title={offer.seller_id === user?.id ? t('p2pAd.cantTradeOwnAd') : ''}
|
||||
>
|
||||
{type === 'buy' ? 'Buy' : 'Sell'} {offer.token}
|
||||
{type === 'buy' ? t('p2pAd.buyToken', { token: offer.token }) : t('p2pAd.sellToken', { token: offer.token })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,7 +289,7 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
{offer.status.toUpperCase()}
|
||||
</Badge>
|
||||
<p className="text-sm text-gray-400">
|
||||
Created: {new Date(offer.created_at).toLocaleDateString()}
|
||||
{t('p2pAd.created', { date: new Date(offer.created_at).toLocaleDateString() })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat';
|
||||
|
||||
interface BlockTradeRequest {
|
||||
@@ -78,6 +79,7 @@ export function BlockTrade() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [requests, setRequests] = useState<BlockTradeRequest[]>([]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const fiatSymbol = SUPPORTED_FIATS.find(f => f.code === fiat)?.symbol || '';
|
||||
const minAmount = MINIMUM_BLOCK_AMOUNTS[token];
|
||||
@@ -103,13 +105,13 @@ export function BlockTrade() {
|
||||
|
||||
const handleSubmitRequest = async () => {
|
||||
if (!user) {
|
||||
toast.error('Please login to submit a block trade request');
|
||||
toast.error(t('p2pBlock.loginRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const amountNum = parseFloat(amount);
|
||||
if (isNaN(amountNum) || amountNum < minAmount) {
|
||||
toast.error(`Minimum amount for ${token} block trade is ${minAmount.toLocaleString()} ${token}`);
|
||||
toast.error(t('p2pBlock.minimumError', { token, amount: minAmount.toLocaleString() }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ export function BlockTrade() {
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Block trade request submitted! Our OTC desk will contact you within 24 hours.');
|
||||
toast.success(t('p2pBlock.requestSubmitted'));
|
||||
setShowRequestModal(false);
|
||||
setAmount('');
|
||||
setTargetPrice('');
|
||||
@@ -142,7 +144,7 @@ export function BlockTrade() {
|
||||
setRequests(prev => [data, ...prev]);
|
||||
} catch (err) {
|
||||
console.error('Block trade request error:', err);
|
||||
toast.error('Failed to submit request');
|
||||
toast.error(t('p2pBlock.failedToSubmit'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -170,14 +172,14 @@ export function BlockTrade() {
|
||||
<Blocks className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg text-white">Block Trade (OTC)</CardTitle>
|
||||
<CardTitle className="text-lg text-white">{t('p2pBlock.title')}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Large volume trades with custom pricing
|
||||
{t('p2pBlock.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
|
||||
VIP
|
||||
{t('p2pBlock.vip')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -186,25 +188,25 @@ export function BlockTrade() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Lock className="w-4 h-4 text-purple-400" />
|
||||
<span>Private Negotiation</span>
|
||||
<span>{t('p2pBlock.privateNegotiation')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Shield className="w-4 h-4 text-green-400" />
|
||||
<span>Escrow Protected</span>
|
||||
<span>{t('p2pBlock.escrowProtected')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Building2 className="w-4 h-4 text-blue-400" />
|
||||
<span>Dedicated Support</span>
|
||||
<span>{t('p2pBlock.dedicatedSupport')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Clock className="w-4 h-4 text-yellow-400" />
|
||||
<span>Flexible Settlement</span>
|
||||
<span>{t('p2pBlock.flexibleSettlement')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Minimum Amounts Info */}
|
||||
<div className="p-3 bg-gray-800/50 rounded-lg">
|
||||
<p className="text-xs text-gray-500 mb-2">Minimum Block Trade Amounts:</p>
|
||||
<p className="text-xs text-gray-500 mb-2">{t('p2pBlock.minimumAmounts')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(MINIMUM_BLOCK_AMOUNTS).map(([t, min]) => (
|
||||
<Badge key={t} variant="outline" className="border-gray-700 text-gray-300">
|
||||
@@ -220,14 +222,14 @@ export function BlockTrade() {
|
||||
onClick={() => setShowRequestModal(true)}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Request Block Trade
|
||||
{t('p2pBlock.requestBlockTrade')}
|
||||
<ChevronRight className="w-4 h-4 ml-auto" />
|
||||
</Button>
|
||||
|
||||
{/* Active Requests */}
|
||||
{requests.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-gray-500">Your Requests:</p>
|
||||
<p className="text-xs text-gray-500">{t('p2pBlock.yourRequests')}</p>
|
||||
{requests.slice(0, 3).map(req => (
|
||||
<div
|
||||
key={req.id}
|
||||
@@ -257,10 +259,10 @@ export function BlockTrade() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Blocks className="w-5 h-5 text-purple-400" />
|
||||
Block Trade Request
|
||||
{t('p2pBlock.requestTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Submit a request for our OTC desk to handle your large volume trade.
|
||||
{t('p2pBlock.requestDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -269,10 +271,10 @@ export function BlockTrade() {
|
||||
<Tabs value={type} onValueChange={(v) => setType(v as 'buy' | 'sell')}>
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
|
||||
<TabsTrigger value="buy" className="data-[state=active]:bg-green-600">
|
||||
Buy
|
||||
{t('p2p.buy')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="data-[state=active]:bg-red-600">
|
||||
Sell
|
||||
{t('p2p.sell')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -280,7 +282,7 @@ export function BlockTrade() {
|
||||
{/* Token & Fiat */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Token</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.token')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -293,7 +295,7 @@ export function BlockTrade() {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Currency</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.currency')}</Label>
|
||||
<Select value={fiat} onValueChange={(v) => setFiat(v as FiatCurrency)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -311,7 +313,7 @@ export function BlockTrade() {
|
||||
|
||||
{/* Amount */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Amount ({token})</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.amountLabel', { token })}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={`Min: ${minAmount.toLocaleString()}`}
|
||||
@@ -320,17 +322,17 @@ export function BlockTrade() {
|
||||
className="bg-gray-800 border-gray-700"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Minimum: {minAmount.toLocaleString()} {token}
|
||||
{t('p2pBlock.minimumLabel', { amount: minAmount.toLocaleString(), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Target Price (Optional) */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Target Price (Optional)</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.targetPrice')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Your desired price per unit"
|
||||
placeholder={t('p2pBlock.targetPricePlaceholder')}
|
||||
value={targetPrice}
|
||||
onChange={(e) => setTargetPrice(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700 pr-16"
|
||||
@@ -343,9 +345,9 @@ export function BlockTrade() {
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Additional Details (Optional)</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2pBlock.additionalDetails')}</Label>
|
||||
<Textarea
|
||||
placeholder="Settlement preferences, timeline, payment methods..."
|
||||
placeholder={t('p2pBlock.detailsPlaceholder')}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700 min-h-[80px]"
|
||||
@@ -356,8 +358,7 @@ export function BlockTrade() {
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5" />
|
||||
<p className="text-xs text-yellow-400">
|
||||
Block trades require KYC verification and may take 24-48 hours to process.
|
||||
Our OTC desk will contact you via email.
|
||||
{t('p2pBlock.kycWarning')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,14 +369,14 @@ export function BlockTrade() {
|
||||
onClick={() => setShowRequestModal(false)}
|
||||
className="border-gray-700"
|
||||
>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={handleSubmitRequest}
|
||||
disabled={isSubmitting || !amount || parseFloat(amount) < minAmount}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
||||
{isSubmitting ? t('p2p.submitting') : t('p2pBlock.submitRequest')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -22,6 +23,7 @@ interface CreateAdProps {
|
||||
}
|
||||
|
||||
export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { account } = useWallet();
|
||||
|
||||
@@ -78,13 +80,13 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
console.log('🔥 handleCreateAd called', { account, user: user?.id });
|
||||
|
||||
if (!account || !user) {
|
||||
toast.error('Please connect your wallet and log in');
|
||||
toast.error(t('p2p.connectWalletAndLogin'));
|
||||
console.log('❌ No account or user', { account, user });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPaymentMethod) {
|
||||
toast.error('Please select a payment method');
|
||||
toast.error(t('p2pCreate.selectPaymentMethodError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,22 +107,22 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
const fiatAmt = parseFloat(fiatAmount);
|
||||
|
||||
if (!cryptoAmt || cryptoAmt <= 0) {
|
||||
toast.error('Invalid crypto amount');
|
||||
toast.error(t('p2pCreate.invalidCryptoAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fiatAmt || fiatAmt <= 0) {
|
||||
toast.error('Invalid fiat amount');
|
||||
toast.error(t('p2pCreate.invalidFiatAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.min_trade_amount && fiatAmt < selectedPaymentMethod.min_trade_amount) {
|
||||
toast.error(`Minimum trade amount: ${selectedPaymentMethod.min_trade_amount} ${fiatCurrency}`);
|
||||
toast.error(t('p2pCreate.minTradeAmount', { amount: selectedPaymentMethod.min_trade_amount, currency: fiatCurrency }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.max_trade_amount && fiatAmt > selectedPaymentMethod.max_trade_amount) {
|
||||
toast.error(`Maximum trade amount: ${selectedPaymentMethod.max_trade_amount} ${fiatCurrency}`);
|
||||
toast.error(t('p2pCreate.maxTradeAmount', { amount: selectedPaymentMethod.max_trade_amount, currency: fiatCurrency }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,16 +154,16 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
if (error) {
|
||||
console.error('❌ Supabase error:', error);
|
||||
toast.error(error.message || 'Failed to create offer');
|
||||
toast.error(error.message || t('p2pCreate.failedToCreate'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Offer created successfully:', data);
|
||||
toast.success('Ad created successfully!');
|
||||
toast.success(t('p2pCreate.adCreated'));
|
||||
onAdCreated();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Create ad error:', error);
|
||||
toast.error('Failed to create offer');
|
||||
toast.error(t('p2pCreate.failedToCreate'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -170,15 +172,15 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Create P2P Offer</CardTitle>
|
||||
<CardTitle className="text-white">{t('p2pCreate.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
Lock your crypto in escrow and set your price
|
||||
{t('p2pCreate.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Ad Type Selection */}
|
||||
<div>
|
||||
<Label>I want to</Label>
|
||||
<Label>{t('p2pCreate.iWantTo')}</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -186,7 +188,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
className={adType === 'sell' ? 'bg-red-600 hover:bg-red-700' : ''}
|
||||
onClick={() => setAdType('sell')}
|
||||
>
|
||||
Sell {token}
|
||||
{t('p2pCreate.sellToken', { token })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -194,20 +196,20 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
className={adType === 'buy' ? 'bg-green-600 hover:bg-green-700' : ''}
|
||||
onClick={() => setAdType('buy')}
|
||||
>
|
||||
Buy {token}
|
||||
{t('p2pCreate.buyToken', { token })}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{adType === 'sell'
|
||||
? 'You will receive fiat payment and send crypto to buyer'
|
||||
: 'You will send fiat payment and receive crypto from seller'}
|
||||
? t('p2pCreate.sellDescription')
|
||||
: t('p2pCreate.buyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Crypto Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="token">Token</Label>
|
||||
<Label htmlFor="token">{t('p2p.token')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -219,14 +221,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="amountCrypto">Amount ({token})</Label>
|
||||
<Label htmlFor="amountCrypto">{t('p2pCreate.amountLabel', { token })}</Label>
|
||||
<Input
|
||||
id="amountCrypto"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={amountCrypto}
|
||||
onChange={e => setAmountCrypto(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2pCreate.amountPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -235,7 +237,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Fiat Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="fiatCurrency">Fiat Currency</Label>
|
||||
<Label htmlFor="fiatCurrency">{t('p2pCreate.fiatCurrency')}</Label>
|
||||
<Select value={fiatCurrency} onValueChange={(v) => setFiatCurrency(v as FiatCurrency)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -260,14 +262,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="fiatAmount">Total Amount ({fiatCurrency})</Label>
|
||||
<Label htmlFor="fiatAmount">{t('p2pCreate.totalFiatAmount', { currency: fiatCurrency })}</Label>
|
||||
<Input
|
||||
id="fiatAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={fiatAmount}
|
||||
onChange={e => setFiatAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2pCreate.amountPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -276,7 +278,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Price Display */}
|
||||
{amountCrypto && fiatAmount && (
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400">Price per {token}</p>
|
||||
<p className="text-sm text-gray-400">{t('p2pCreate.pricePerToken', { token })}</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
{pricePerUnit} {fiatCurrency}
|
||||
</p>
|
||||
@@ -285,10 +287,10 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<Label htmlFor="paymentMethod">Payment Method</Label>
|
||||
<Label htmlFor="paymentMethod">{t('p2pCreate.paymentMethod')}</Label>
|
||||
<Select onValueChange={handlePaymentMethodChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select payment method..." />
|
||||
<SelectValue placeholder={t('p2pCreate.selectPaymentMethod')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethods.map(method => (
|
||||
@@ -303,7 +305,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Dynamic Payment Details Fields */}
|
||||
{selectedPaymentMethod && Object.keys(selectedPaymentMethod.fields).length > 0 && (
|
||||
<div className="space-y-4 p-4 border border-gray-700 rounded-lg">
|
||||
<h3 className="font-semibold text-white">Payment Details</h3>
|
||||
<h3 className="font-semibold text-white">{t('p2pCreate.paymentDetails')}</h3>
|
||||
{Object.entries(selectedPaymentMethod.fields).map(([field, placeholder]) => (
|
||||
<div key={field}>
|
||||
<Label htmlFor={field}>
|
||||
@@ -324,26 +326,26 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{/* Order Limits */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="minOrder">Min Order (optional)</Label>
|
||||
<Label htmlFor="minOrder">{t('p2pCreate.minOrder')}</Label>
|
||||
<Input
|
||||
id="minOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={minOrderAmount}
|
||||
onChange={e => setMinOrderAmount(e.target.value)}
|
||||
placeholder="Minimum amount (optional)"
|
||||
placeholder={t('p2pCreate.minOrderPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="maxOrder">Max Order (optional)</Label>
|
||||
<Label htmlFor="maxOrder">{t('p2pCreate.maxOrder')}</Label>
|
||||
<Input
|
||||
id="maxOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={maxOrderAmount}
|
||||
onChange={e => setMaxOrderAmount(e.target.value)}
|
||||
placeholder="Maximum amount (optional)"
|
||||
placeholder={t('p2pCreate.maxOrderPlaceholder')}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -351,16 +353,16 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
|
||||
{/* Time Limit */}
|
||||
<div>
|
||||
<Label htmlFor="timeLimit">Payment Time Limit (minutes)</Label>
|
||||
<Label htmlFor="timeLimit">{t('p2pCreate.paymentTimeLimit')}</Label>
|
||||
<Select value={timeLimit.toString()} onValueChange={(v) => setTimeLimit(parseInt(v))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="15">15 minutes</SelectItem>
|
||||
<SelectItem value="30">30 minutes</SelectItem>
|
||||
<SelectItem value="60">1 hour</SelectItem>
|
||||
<SelectItem value="120">2 hours</SelectItem>
|
||||
<SelectItem value="15">{t('p2pCreate.15min')}</SelectItem>
|
||||
<SelectItem value="30">{t('p2pCreate.30min')}</SelectItem>
|
||||
<SelectItem value="60">{t('p2pCreate.1hour')}</SelectItem>
|
||||
<SelectItem value="120">{t('p2pCreate.2hours')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -373,10 +375,10 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating offer & locking escrow...
|
||||
{t('p2pCreate.creatingOffer')}
|
||||
</>
|
||||
) : (
|
||||
'Create Offer'
|
||||
t('p2pCreate.createOffer')
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -46,6 +47,7 @@ interface DepositModalProps {
|
||||
type DepositStep = 'select' | 'send' | 'verify' | 'success';
|
||||
|
||||
export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
const { balances, signTransaction } = useWallet();
|
||||
|
||||
@@ -89,10 +91,10 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
try {
|
||||
await navigator.clipboard.writeText(platformWallet);
|
||||
setCopied(true);
|
||||
toast.success('Address copied to clipboard');
|
||||
toast.success(t('p2pDeposit.addressCopied'));
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error('Failed to copy address');
|
||||
toast.error(t('p2pDeposit.failedToCopy'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,13 +106,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
const handleSendDeposit = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast.error('Please connect your wallet');
|
||||
toast.error(t('p2pDeposit.connectWallet'));
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Please enter a valid amount');
|
||||
toast.error(t('p2pDeposit.enterValidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
tx = api.tx.assets.transfer(assetId, platformWallet, amountBN);
|
||||
}
|
||||
|
||||
toast.info('Please sign the transaction in your wallet...');
|
||||
toast.info(t('p2pDeposit.signTransaction'));
|
||||
|
||||
// Sign and send
|
||||
const hash = await signTransaction(tx);
|
||||
@@ -139,11 +141,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
if (hash) {
|
||||
setTxHash(hash);
|
||||
setStep('verify');
|
||||
toast.success('Transaction sent! Please verify your deposit.');
|
||||
toast.success(t('p2pDeposit.txSent'));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Deposit transaction error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Transaction failed';
|
||||
const message = error instanceof Error ? error.message : t('p2pDeposit.txFailed');
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -152,13 +154,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
const handleVerifyDeposit = async () => {
|
||||
if (!txHash) {
|
||||
toast.error('Please enter the transaction hash');
|
||||
toast.error(t('p2pDeposit.enterTxHash'));
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Invalid amount');
|
||||
toast.error(t('p2pDeposit.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,19 +178,19 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Verification failed');
|
||||
throw new Error(error.message || t('p2pDeposit.verificationFailed'));
|
||||
}
|
||||
|
||||
if (data?.success) {
|
||||
toast.success(`Deposit verified! ${data.amount} ${token} added to your balance.`);
|
||||
toast.success(t('p2pDeposit.depositVerified', { amount: data.amount, token }));
|
||||
setStep('success');
|
||||
onSuccess?.();
|
||||
} else {
|
||||
throw new Error(data?.error || 'Verification failed');
|
||||
throw new Error(data?.error || t('p2pDeposit.verificationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verify deposit error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Verification failed';
|
||||
const message = error instanceof Error ? error.message : t('p2pDeposit.verificationFailed');
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setVerifying(false);
|
||||
@@ -201,20 +203,20 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Label>{t('p2pDeposit.selectToken')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
<SelectItem value="HEZ">{t('p2pDeposit.hezNative')}</SelectItem>
|
||||
<SelectItem value="PEZ">{t('p2pDeposit.pez')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Amount to Deposit</Label>
|
||||
<Label>{t('p2pDeposit.amountToDeposit')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -229,27 +231,26 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wallet Balance: {parseFloat(getAvailableBalance()).toFixed(4)} {token}
|
||||
{t('p2pDeposit.walletBalance', { amount: parseFloat(getAvailableBalance()).toFixed(4), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Wallet className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You will send {token} from your connected wallet to the P2P platform escrow.
|
||||
After confirmation, the amount will be credited to your P2P internal balance.
|
||||
{t('p2pDeposit.depositInfo', { token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setStep('send')}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
{t('continue')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
@@ -263,7 +264,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<QrCode className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">Send {amount} {token} to:</p>
|
||||
<p className="text-sm font-medium">{t('p2pDeposit.sendAmountTo', { amount, token })}</p>
|
||||
</div>
|
||||
|
||||
{platformWallet ? (
|
||||
@@ -280,12 +281,12 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
|
||||
Copied!
|
||||
{t('p2pDeposit.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Copy Address
|
||||
{t('p2pDeposit.copyAddress')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -298,8 +299,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Only send {token} on the PezkuwiChain network. Sending other tokens or using
|
||||
other networks will result in permanent loss of funds.
|
||||
{t('p2pDeposit.networkWarning', { token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -309,7 +309,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
className="flex-1"
|
||||
onClick={() => setStep('select')}
|
||||
>
|
||||
Back
|
||||
{t('back')}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
@@ -319,11 +319,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Sending...
|
||||
{t('p2pDeposit.sending')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Send {amount} {token}
|
||||
{t('p2pDeposit.sendToken', { amount, token })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -337,12 +337,12 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<AlertDescription>
|
||||
Transaction sent! Please verify your deposit to credit your P2P balance.
|
||||
{t('p2pDeposit.txSentVerify')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Transaction Hash</Label>
|
||||
<Label>{t('p2pDeposit.txHash')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={txHash}
|
||||
@@ -364,11 +364,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Token</p>
|
||||
<p className="text-muted-foreground">{t('p2pDeposit.tokenLabel')}</p>
|
||||
<p className="font-semibold">{token}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Amount</p>
|
||||
<p className="text-muted-foreground">{t('p2pDeposit.amountLabel')}</p>
|
||||
<p className="font-semibold">{amount}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +376,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleVerifyDeposit}
|
||||
@@ -385,10 +385,10 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
{verifying ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Verifying...
|
||||
{t('p2pDeposit.verifying')}
|
||||
</>
|
||||
) : (
|
||||
'Verify Deposit'
|
||||
t('p2pDeposit.verifyDeposit')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -404,22 +404,21 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Deposit Successful!
|
||||
{t('p2pDeposit.depositSuccess')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{amount} {token} has been added to your P2P internal balance.
|
||||
{t('p2pDeposit.addedToBalance', { amount, token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can now create sell offers or trade P2P using your internal balance.
|
||||
No blockchain fees during P2P trades!
|
||||
{t('p2pDeposit.successInfo')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
{t('p2pDeposit.done')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -432,13 +431,13 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5" />
|
||||
Deposit to P2P Balance
|
||||
{t('p2pDeposit.title')}
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'select' && 'Deposit crypto from your wallet to P2P internal balance'}
|
||||
{step === 'send' && 'Send tokens to the platform escrow wallet'}
|
||||
{step === 'verify' && 'Verify your transaction to credit your balance'}
|
||||
{step === 'select' && t('p2pDeposit.selectStep')}
|
||||
{step === 'send' && t('p2pDeposit.sendStep')}
|
||||
{step === 'verify' && t('p2pDeposit.verifyStep')}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
@@ -39,16 +39,18 @@ interface EvidenceFile {
|
||||
type: 'image' | 'document';
|
||||
}
|
||||
|
||||
const DISPUTE_REASONS = [
|
||||
{ value: 'payment_not_received', label: 'Payment not received' },
|
||||
{ value: 'wrong_amount', label: 'Wrong amount received' },
|
||||
{ value: 'seller_not_responding', label: 'Seller not responding' },
|
||||
{ value: 'buyer_not_responding', label: 'Buyer not responding' },
|
||||
{ value: 'fraudulent_behavior', label: 'Fraudulent behavior' },
|
||||
{ value: 'fake_payment_proof', label: 'Fake payment proof' },
|
||||
{ value: 'account_mismatch', label: 'Payment account name mismatch' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
const DISPUTE_REASON_KEYS: Record<string, string> = {
|
||||
payment_not_received: 'p2pDispute.paymentNotReceived',
|
||||
wrong_amount: 'p2pDispute.wrongAmount',
|
||||
seller_not_responding: 'p2pDispute.sellerNotResponding',
|
||||
buyer_not_responding: 'p2pDispute.buyerNotResponding',
|
||||
fraudulent_behavior: 'p2pDispute.fraudulentBehavior',
|
||||
fake_payment_proof: 'p2pDispute.fakePaymentProof',
|
||||
account_mismatch: 'p2pDispute.accountMismatch',
|
||||
other: 'p2pDispute.other',
|
||||
};
|
||||
|
||||
const DISPUTE_REASONS = Object.keys(DISPUTE_REASON_KEYS);
|
||||
|
||||
export function DisputeModal({
|
||||
isOpen,
|
||||
@@ -59,7 +61,7 @@ export function DisputeModal({
|
||||
counterpartyWallet,
|
||||
isBuyer,
|
||||
}: DisputeModalProps) {
|
||||
useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [reason, setReason] = useState('');
|
||||
@@ -69,11 +71,11 @@ export function DisputeModal({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Filter reasons based on role
|
||||
const availableReasons = DISPUTE_REASONS.filter((r) => {
|
||||
const availableReasons = DISPUTE_REASONS.filter((value) => {
|
||||
if (isBuyer) {
|
||||
return r.value !== 'buyer_not_responding' && r.value !== 'payment_not_received';
|
||||
return value !== 'buyer_not_responding' && value !== 'payment_not_received';
|
||||
} else {
|
||||
return r.value !== 'seller_not_responding' && r.value !== 'fake_payment_proof';
|
||||
return value !== 'seller_not_responding' && value !== 'fake_payment_proof';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,12 +87,12 @@ export function DisputeModal({
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
if (evidenceFiles.length + newFiles.length >= 5) {
|
||||
toast.error('Maximum 5 evidence files allowed');
|
||||
toast.error(t('p2pDispute.maxFilesError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error(`File ${file.name} is too large (max 10MB)`);
|
||||
toast.error(t('p2pDispute.fileTooLarge', { name: file.name }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,17 +155,17 @@ export function DisputeModal({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!reason) {
|
||||
toast.error('Please select a reason');
|
||||
toast.error(t('p2pDispute.selectReasonError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description || description.length < 20) {
|
||||
toast.error('Please provide a detailed description (at least 20 characters)');
|
||||
toast.error(t('p2pDispute.descriptionError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!termsAccepted) {
|
||||
toast.error('Please accept the terms and conditions');
|
||||
toast.error(t('p2pDispute.acceptTermsError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,11 +239,11 @@ export function DisputeModal({
|
||||
await supabase.from('p2p_notifications').insert(adminNotifications);
|
||||
}
|
||||
|
||||
toast.success('Dispute opened successfully');
|
||||
toast.success(t('p2pDispute.disputeOpened'));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to open dispute:', error);
|
||||
toast.error('Failed to open dispute. Please try again.');
|
||||
toast.error(t('p2pDispute.failedToOpen'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -265,26 +267,25 @@ export function DisputeModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-500">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Open Dispute
|
||||
{t('p2pDispute.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please provide details about the issue. Our support team will review your case
|
||||
and contact both parties for resolution.
|
||||
{t('p2pDispute.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Reason Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">Reason for Dispute *</Label>
|
||||
<Label htmlFor="reason">{t('p2pDispute.reasonLabel')}</Label>
|
||||
<Select value={reason} onValueChange={setReason}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a reason..." />
|
||||
<SelectValue placeholder={t('p2pDispute.selectReason')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableReasons.map((r) => (
|
||||
<SelectItem key={r.value} value={r.value}>
|
||||
{r.label}
|
||||
{availableReasons.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(DISPUTE_REASON_KEYS[value])}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -294,13 +295,13 @@ export function DisputeModal({
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">
|
||||
Detailed Description * <span className="text-muted-foreground text-xs">(min 20 chars)</span>
|
||||
{t('p2pDispute.detailedDescription')} <span className="text-muted-foreground text-xs">{t('p2pDispute.minChars')}</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Please describe the issue in detail. Include relevant transaction IDs, timestamps, and any communication with the counterparty..."
|
||||
placeholder={t('p2pDispute.descriptionPlaceholder')}
|
||||
rows={4}
|
||||
maxLength={2000}
|
||||
/>
|
||||
@@ -311,7 +312,7 @@ export function DisputeModal({
|
||||
|
||||
{/* Evidence Upload */}
|
||||
<div className="space-y-2">
|
||||
<Label>Evidence (Optional - max 5 files, 10MB each)</Label>
|
||||
<Label>{t('p2pDispute.evidenceLabel')}</Label>
|
||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4">
|
||||
<input
|
||||
type="file"
|
||||
@@ -330,10 +331,10 @@ export function DisputeModal({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={evidenceFiles.length >= 5}
|
||||
>
|
||||
Upload Evidence
|
||||
{t('p2pDispute.uploadEvidence')}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Screenshots, bank statements, chat logs, receipts
|
||||
{t('p2pDispute.evidenceTypes')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +350,7 @@ export function DisputeModal({
|
||||
{evidence.type === 'image' ? (
|
||||
<img
|
||||
src={evidence.preview}
|
||||
alt="Evidence"
|
||||
alt={t('p2pDispute.evidenceAlt')}
|
||||
className="w-10 h-10 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
@@ -376,13 +377,13 @@ export function DisputeModal({
|
||||
<AlertTriangle className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-amber-800 dark:text-amber-200">
|
||||
Important Notice
|
||||
{t('p2pDispute.importantNotice')}
|
||||
</p>
|
||||
<ul className="text-amber-700 dark:text-amber-300 text-xs mt-1 space-y-1">
|
||||
<li>• False disputes may result in account restrictions</li>
|
||||
<li>• Resolution typically takes 1-3 business days</li>
|
||||
<li>• Both parties can submit evidence</li>
|
||||
<li>• Admin decision is final</li>
|
||||
<li>• {t('p2pDispute.falseDisputes')}</li>
|
||||
<li>• {t('p2pDispute.resolutionTime')}</li>
|
||||
<li>• {t('p2pDispute.bothParties')}</li>
|
||||
<li>• {t('p2pDispute.adminFinal')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,22 +397,21 @@ export function DisputeModal({
|
||||
onCheckedChange={(checked) => setTermsAccepted(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="terms" className="text-sm leading-tight cursor-pointer">
|
||||
I confirm that the information provided is accurate and understand that
|
||||
false claims may result in penalties.
|
||||
{t('p2pDispute.termsCheckbox')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={handleClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !reason || !description || !termsAccepted}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Open Dispute'}
|
||||
{isSubmitting ? t('p2pDispute.submitting') : t('p2pDispute.openDispute')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { supabase } from '@/lib/supabase';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat';
|
||||
|
||||
interface BestOffer {
|
||||
@@ -67,6 +68,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -139,17 +141,17 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
// Handle express trade
|
||||
const handleExpressTrade = async () => {
|
||||
if (!user) {
|
||||
toast.error('Please login to trade');
|
||||
toast.error(t('p2pExpress.loginRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bestOffer) {
|
||||
toast.error('No offers available');
|
||||
toast.error(t('p2pExpress.noOffersAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cryptoAmount > bestOffer.remaining_amount) {
|
||||
toast.error(`Maximum available: ${bestOffer.remaining_amount} ${token}`);
|
||||
toast.error(t('p2pExpress.maxAvailable', { amount: bestOffer.remaining_amount, token }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,7 +173,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
throw new Error(response.error || 'Failed to start trade');
|
||||
}
|
||||
|
||||
toast.success('Express trade started!');
|
||||
toast.success(t('p2pExpress.tradeStarted'));
|
||||
|
||||
if (onTradeStarted) {
|
||||
onTradeStarted(response.trade_id);
|
||||
@@ -195,12 +197,12 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<Zap className="w-5 h-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg text-white">Express Mode</CardTitle>
|
||||
<p className="text-xs text-gray-400">Instant best-rate matching</p>
|
||||
<CardTitle className="text-lg text-white">{t('p2pExpress.title')}</CardTitle>
|
||||
<p className="text-xs text-gray-400">{t('p2pExpress.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
|
||||
Fast
|
||||
{t('p2pExpress.fast')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -209,10 +211,10 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<Tabs value={mode} onValueChange={(v) => setMode(v as 'buy' | 'sell')}>
|
||||
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
|
||||
<TabsTrigger value="buy" className="data-[state=active]:bg-green-600">
|
||||
Buy {token}
|
||||
{t('p2pExpress.buyToken', { token })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="data-[state=active]:bg-red-600">
|
||||
Sell {token}
|
||||
{t('p2pExpress.sellToken', { token })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -220,7 +222,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
{/* Token & Fiat Selection */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Crypto</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.crypto')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -233,7 +235,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400 text-xs">Currency</Label>
|
||||
<Label className="text-gray-400 text-xs">{t('p2p.currency')}</Label>
|
||||
<Select value={fiat} onValueChange={(v) => setFiat(v as FiatCurrency)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700">
|
||||
<SelectValue />
|
||||
@@ -253,7 +255,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-gray-400 text-xs">
|
||||
{inputType === 'fiat' ? `Amount (${fiat})` : `Amount (${token})`}
|
||||
{t('p2pExpress.amountLabel', { unit: inputType === 'fiat' ? fiat : token })}
|
||||
</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -261,7 +263,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
className="text-xs text-yellow-400 h-auto p-0"
|
||||
onClick={() => setInputType(inputType === 'fiat' ? 'crypto' : 'fiat')}
|
||||
>
|
||||
Switch to {inputType === 'fiat' ? token : fiat}
|
||||
{t('p2pExpress.switchTo', { unit: inputType === 'fiat' ? token : fiat })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -282,7 +284,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
{bestOffer && parseFloat(amount) > 0 && (
|
||||
<div className="p-3 bg-gray-800/50 rounded-lg space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">You {mode === 'buy' ? 'pay' : 'receive'}</span>
|
||||
<span className="text-gray-400">{mode === 'buy' ? t('p2pExpress.youPay') : t('p2pExpress.youReceive')}</span>
|
||||
<span className="text-white font-medium">
|
||||
{fiatSymbol}{fiatAmount.toLocaleString(undefined, { maximumFractionDigits: 2 })} {fiat}
|
||||
</span>
|
||||
@@ -291,31 +293,31 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<ArrowRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">You {mode === 'buy' ? 'receive' : 'send'}</span>
|
||||
<span className="text-gray-400">{mode === 'buy' ? t('p2pExpress.youReceive') : t('p2pExpress.youSend')}</span>
|
||||
<span className="text-white font-medium">
|
||||
{cryptoAmount.toLocaleString(undefined, { maximumFractionDigits: 6 })} {token}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-700 space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Rate</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.rate')}</span>
|
||||
<span className="text-gray-300">
|
||||
1 {token} = {fiatSymbol}{bestOffer.price_per_unit.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Merchant Rating</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.merchantRating')}</span>
|
||||
<span className="text-yellow-400 flex items-center gap-1">
|
||||
<Star className="w-3 h-3" />
|
||||
{bestOffer.seller_reputation}% ({bestOffer.seller_completed_trades} trades)
|
||||
{bestOffer.seller_reputation}% ({t('p2p.trades', { count: bestOffer.seller_completed_trades })})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Payment</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.payment')}</span>
|
||||
<span className="text-gray-300">{bestOffer.payment_method_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Time Limit</span>
|
||||
<span className="text-gray-500">{t('p2pExpress.timeLimit')}</span>
|
||||
<span className="text-gray-300 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{bestOffer.time_limit_minutes} min
|
||||
@@ -330,7 +332,7 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm text-red-400">
|
||||
No offers available for this pair
|
||||
{t('p2pExpress.noOffers')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -343,11 +345,11 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
disabled={!bestOffer || isLoading || isProcessing || !user}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>Processing...</>
|
||||
<>{t('p2pExpress.processing')}</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
{mode === 'buy' ? 'Buy' : 'Sell'} {token} Instantly
|
||||
{mode === 'buy' ? t('p2pExpress.buyInstantly', { token }) : t('p2pExpress.sellInstantly', { token })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -356,11 +358,11 @@ export function ExpressMode({ onTradeStarted }: ExpressModeProps) {
|
||||
<div className="flex items-center justify-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Shield className="w-3 h-3 text-green-400" />
|
||||
Escrow Protected
|
||||
{t('p2p.escrowProtected')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3 text-blue-400" />
|
||||
Verified Merchants
|
||||
{t('p2p.verifiedMerchants')}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -19,6 +20,7 @@ interface InternalBalanceCardProps {
|
||||
}
|
||||
|
||||
export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [balances, setBalances] = useState<InternalBalance[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -73,7 +75,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Wallet className="h-5 w-5" />
|
||||
P2P Internal Balance
|
||||
{t('p2pBalance.title')}
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -85,15 +87,15 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Internal balance for P2P trading. Deposit to start selling.
|
||||
{t('p2pBalance.subtitle')}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{balances.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Wallet className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">No balance yet</p>
|
||||
<p className="text-xs">Deposit crypto to start P2P trading</p>
|
||||
<p className="text-sm">{t('p2pBalance.noBalance')}</p>
|
||||
<p className="text-xs">{t('p2pBalance.depositToStart')}</p>
|
||||
</div>
|
||||
) : (
|
||||
balances.map((balance) => (
|
||||
@@ -111,7 +113,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<span className="font-semibold">{balance.token}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Total: {formatBalance(balance.total_balance)}
|
||||
{t('p2pBalance.total', { amount: formatBalance(balance.total_balance) })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +121,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center gap-2">
|
||||
<Unlock className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Available</p>
|
||||
<p className="text-muted-foreground text-xs">{t('p2pBalance.available')}</p>
|
||||
<p className="font-medium text-green-600">
|
||||
{formatBalance(balance.available_balance)}
|
||||
</p>
|
||||
@@ -128,7 +130,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Locked (Escrow)</p>
|
||||
<p className="text-muted-foreground text-xs">{t('p2pBalance.lockedEscrow')}</p>
|
||||
<p className="font-medium text-yellow-600">
|
||||
{formatBalance(balance.locked_balance)}
|
||||
</p>
|
||||
@@ -138,11 +140,11 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
|
||||
<div className="mt-3 pt-3 border-t grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span>Total Deposited: </span>
|
||||
<span>{t('p2pBalance.totalDeposited')}</span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_deposited, 2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total Withdrawn: </span>
|
||||
<span>{t('p2pBalance.totalWithdrawn')}</span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_withdrawn, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,7 +160,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
onClick={onDeposit}
|
||||
>
|
||||
<ArrowDownToLine className="h-4 w-4 mr-2" />
|
||||
Deposit
|
||||
{t('p2pBalance.deposit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -167,7 +169,7 @@ export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCa
|
||||
disabled={balances.every(b => b.available_balance <= 0)}
|
||||
>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Withdraw
|
||||
{t('p2pBalance.withdraw')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -105,6 +106,7 @@ const TIER_COLORS = {
|
||||
};
|
||||
|
||||
export function MerchantApplication() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [requirements, setRequirements] = useState<TierRequirements[]>(DEFAULT_REQUIREMENTS);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ completed_trades: 0, completion_rate: 0, volume_30d: 0 });
|
||||
@@ -217,7 +219,7 @@ export function MerchantApplication() {
|
||||
|
||||
if (data && data[0]) {
|
||||
if (data[0].success) {
|
||||
toast.success('Application submitted successfully!');
|
||||
toast.success(t('p2pMerchant.applicationSubmitted'));
|
||||
setApplyModalOpen(false);
|
||||
fetchData();
|
||||
} else {
|
||||
@@ -226,7 +228,7 @@ export function MerchantApplication() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying for tier:', error);
|
||||
toast.error('Failed to submit application');
|
||||
toast.error(t('p2pMerchant.applicationFailed'));
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
@@ -257,9 +259,9 @@ export function MerchantApplication() {
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Crown className="h-5 w-5 text-kurdish-green" />
|
||||
Your Merchant Status
|
||||
{t('p2pMerchant.yourStatus')}
|
||||
</CardTitle>
|
||||
<CardDescription>Current tier and application status</CardDescription>
|
||||
<CardDescription>{t('p2pMerchant.currentTierStatus')}</CardDescription>
|
||||
</div>
|
||||
<MerchantTierBadge tier={currentTier.tier} size="lg" />
|
||||
</div>
|
||||
@@ -268,15 +270,15 @@ export function MerchantApplication() {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">{userStats.completed_trades}</p>
|
||||
<p className="text-sm text-muted-foreground">Completed Trades</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.completedTrades')}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">{userStats.completion_rate.toFixed(1)}%</p>
|
||||
<p className="text-sm text-muted-foreground">Completion Rate</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.completionRate')}</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-background/50 rounded-lg">
|
||||
<p className="text-2xl font-bold">${userStats.volume_30d.toLocaleString()}</p>
|
||||
<p className="text-sm text-muted-foreground">30-Day Volume</p>
|
||||
<p className="text-sm text-muted-foreground">{t('p2pMerchant.thirtyDayVolume')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +286,7 @@ export function MerchantApplication() {
|
||||
<Alert className="mt-4 bg-yellow-500/10 border-yellow-500/30">
|
||||
<Clock className="h-4 w-4 text-yellow-500" />
|
||||
<AlertDescription className="text-yellow-500">
|
||||
Your application for {currentTier.applied_for_tier?.toUpperCase()} tier is pending review.
|
||||
{t('p2pMerchant.pendingReview', { tier: currentTier.applied_for_tier?.toUpperCase() })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -316,7 +318,7 @@ export function MerchantApplication() {
|
||||
{/* Current tier indicator */}
|
||||
{isCurrentTier && (
|
||||
<div className="absolute top-0 right-0 bg-kurdish-green text-white text-xs px-2 py-0.5 rounded-bl">
|
||||
Current
|
||||
{t('p2pMerchant.current')}
|
||||
</div>
|
||||
)}
|
||||
{isPastTier && (
|
||||
@@ -343,7 +345,7 @@ export function MerchantApplication() {
|
||||
{/* Trades */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Completed Trades</span>
|
||||
<span>{t('p2pMerchant.completedTradesReq')}</span>
|
||||
<span>{userStats.completed_trades} / {tier.min_trades}</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -356,7 +358,7 @@ export function MerchantApplication() {
|
||||
{tier.min_completion_rate > 0 && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Completion Rate</span>
|
||||
<span>{t('p2pMerchant.completionRateReq')}</span>
|
||||
<span>{userStats.completion_rate.toFixed(1)}% / {tier.min_completion_rate}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -370,7 +372,7 @@ export function MerchantApplication() {
|
||||
{tier.min_volume_30d > 0 && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>30-Day Volume</span>
|
||||
<span>{t('p2pMerchant.volumeReq')}</span>
|
||||
<span>${userStats.volume_30d.toLocaleString()} / ${tier.min_volume_30d.toLocaleString()}</span>
|
||||
</div>
|
||||
<Progress
|
||||
@@ -383,20 +385,20 @@ export function MerchantApplication() {
|
||||
|
||||
{/* Benefits */}
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<p className="text-xs text-muted-foreground mb-2">Benefits:</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{t('p2pMerchant.benefits')}</p>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>Up to {tier.max_pending_orders} pending orders</span>
|
||||
<span>{t('p2pMerchant.pendingOrders', { count: tier.max_pending_orders })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>Max ${tier.max_order_amount.toLocaleString()} per trade</span>
|
||||
<span>{t('p2pMerchant.maxPerTrade', { amount: tier.max_order_amount.toLocaleString() })}</span>
|
||||
</div>
|
||||
{tier.featured_ads_allowed > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-kurdish-green" />
|
||||
<span>{tier.featured_ads_allowed} featured ads</span>
|
||||
<span>{t('p2pMerchant.featuredAds', { count: tier.featured_ads_allowed })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -406,7 +408,7 @@ export function MerchantApplication() {
|
||||
{tier.deposit_required > 0 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Lock className="h-3 w-3" />
|
||||
<span>Requires {tier.deposit_required.toLocaleString()} {tier.deposit_token} deposit</span>
|
||||
<span>{t('p2pMerchant.depositRequired', { amount: tier.deposit_required.toLocaleString(), token: tier.deposit_token })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -417,7 +419,7 @@ export function MerchantApplication() {
|
||||
size="sm"
|
||||
onClick={() => openApplyModal(tier.tier)}
|
||||
>
|
||||
Apply for Upgrade
|
||||
{t('p2pMerchant.applyForUpgrade')}
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -433,10 +435,10 @@ export function MerchantApplication() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-kurdish-green" />
|
||||
Apply for {selectedTier?.toUpperCase()} Tier
|
||||
{t('p2pMerchant.applyForTier', { tier: selectedTier?.toUpperCase() })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Submit your application for tier upgrade. Our team will review it shortly.
|
||||
{t('p2pMerchant.applyDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -444,20 +446,20 @@ export function MerchantApplication() {
|
||||
<div className="space-y-4">
|
||||
{/* Requirements check */}
|
||||
<div className="bg-muted p-4 rounded-lg space-y-2">
|
||||
<p className="font-medium text-sm">Requirements Met:</p>
|
||||
<p className="font-medium text-sm">{t('p2pMerchant.requirementsMet')}</p>
|
||||
{requirements.find(r => r.tier === selectedTier) && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Completed trades requirement</span>
|
||||
<span>{t('p2pMerchant.completedTradesRequirement')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Completion rate requirement</span>
|
||||
<span>{t('p2pMerchant.completionRateRequirement')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>30-day volume requirement</span>
|
||||
<span>{t('p2pMerchant.volumeRequirement')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -468,12 +470,10 @@ export function MerchantApplication() {
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This tier requires a deposit of{' '}
|
||||
<strong>
|
||||
{requirements.find(r => r.tier === selectedTier)?.deposit_required.toLocaleString()}{' '}
|
||||
{requirements.find(r => r.tier === selectedTier)?.deposit_token}
|
||||
</strong>
|
||||
. You will be prompted to complete the deposit after approval.
|
||||
{t('p2pMerchant.depositInfo', {
|
||||
amount: requirements.find(r => r.tier === selectedTier)?.deposit_required.toLocaleString(),
|
||||
token: requirements.find(r => r.tier === selectedTier)?.deposit_token
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -482,7 +482,7 @@ export function MerchantApplication() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setApplyModalOpen(false)}>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-kurdish-green hover:bg-kurdish-green-dark"
|
||||
@@ -494,7 +494,7 @@ export function MerchantApplication() {
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Submit Application
|
||||
{t('p2pMerchant.submitApplication')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Diamond, Star, Shield } from 'lucide-react';
|
||||
@@ -12,25 +13,28 @@ interface MerchantTierBadgeProps {
|
||||
|
||||
const TIER_CONFIG = {
|
||||
lite: {
|
||||
label: 'Lite',
|
||||
labelKey: 'p2pTier.lite',
|
||||
icon: Shield,
|
||||
className: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
||||
iconClassName: 'text-gray-400',
|
||||
description: 'Basic verified trader'
|
||||
descKey: 'p2pTier.liteDesc',
|
||||
merchantKey: 'p2pTier.liteMerchant'
|
||||
},
|
||||
super: {
|
||||
label: 'Super',
|
||||
labelKey: 'p2pTier.super',
|
||||
icon: Star,
|
||||
className: 'bg-yellow-500/20 text-yellow-500 border-yellow-500/30',
|
||||
iconClassName: 'text-yellow-500',
|
||||
description: 'Professional trader with 20+ trades and 90%+ completion rate'
|
||||
descKey: 'p2pTier.superDesc',
|
||||
merchantKey: 'p2pTier.superMerchant'
|
||||
},
|
||||
diamond: {
|
||||
label: 'Diamond',
|
||||
labelKey: 'p2pTier.diamond',
|
||||
icon: Diamond,
|
||||
className: 'bg-purple-500/20 text-purple-500 border-purple-500/30',
|
||||
iconClassName: 'text-purple-500',
|
||||
description: 'Elite merchant with 100+ trades and 95%+ completion rate'
|
||||
descKey: 'p2pTier.diamondDesc',
|
||||
merchantKey: 'p2pTier.diamondMerchant'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +58,7 @@ export function MerchantTierBadge({
|
||||
size = 'md',
|
||||
showLabel = true
|
||||
}: MerchantTierBadgeProps) {
|
||||
const { t } = useTranslation();
|
||||
const config = TIER_CONFIG[tier];
|
||||
const sizeConfig = SIZE_CONFIG[size];
|
||||
const Icon = config.icon;
|
||||
@@ -67,12 +72,12 @@ export function MerchantTierBadge({
|
||||
className={`${config.className} ${sizeConfig.badge} gap-1 cursor-help`}
|
||||
>
|
||||
<Icon className={`${sizeConfig.icon} ${config.iconClassName}`} />
|
||||
{showLabel && <span>{config.label}</span>}
|
||||
{showLabel && <span>{t(config.labelKey)}</span>}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{config.label} Merchant</p>
|
||||
<p className="text-xs text-muted-foreground">{config.description}</p>
|
||||
<p className="font-medium">{t(config.merchantKey)}</p>
|
||||
<p className="text-xs text-muted-foreground">{t(config.descKey)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -87,6 +92,7 @@ export function MerchantTierIcon({
|
||||
tier: MerchantTier;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const config = TIER_CONFIG[tier];
|
||||
const sizeConfig = SIZE_CONFIG[size];
|
||||
const Icon = config.icon;
|
||||
@@ -100,7 +106,7 @@ export function MerchantTierIcon({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{config.label} Merchant</p>
|
||||
<p className="font-medium">{t(config.merchantKey)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -37,6 +38,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
export function NotificationBell() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
@@ -173,13 +175,13 @@ export function NotificationBell() {
|
||||
// Format time ago
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const seconds = Math.floor((Date.now() - new Date(dateString).getTime()) / 1000);
|
||||
if (seconds < 60) return 'Just now';
|
||||
if (seconds < 60) return t('p2p.justNow');
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (minutes < 60) return t('p2p.minutesAgo', { count: minutes });
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (hours < 24) return t('p2p.hoursAgo', { count: hours });
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
return t('p2p.daysAgo', { count: days });
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
@@ -206,7 +208,7 @@ export function NotificationBell() {
|
||||
className="w-80 bg-gray-900 border-gray-800"
|
||||
>
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
<span className="text-white">Notifications</span>
|
||||
<span className="text-white">{t('p2pNotif.title')}</span>
|
||||
{unreadCount > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -215,7 +217,7 @@ export function NotificationBell() {
|
||||
className="text-xs text-gray-400 hover:text-white h-auto py-1"
|
||||
>
|
||||
<CheckCheck className="w-3 h-3 mr-1" />
|
||||
Mark all read
|
||||
{t('p2pNotif.markAllRead')}
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
@@ -229,7 +231,7 @@ export function NotificationBell() {
|
||||
) : notifications.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<Bell className="w-8 h-8 mb-2" />
|
||||
<p className="text-sm">No notifications</p>
|
||||
<p className="text-sm">{t('p2pNotif.noNotifications')}</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
@@ -274,7 +276,7 @@ export function NotificationBell() {
|
||||
}}
|
||||
className="justify-center text-gray-400 hover:text-white cursor-pointer"
|
||||
>
|
||||
View all trades
|
||||
{t('p2pNotif.viewAllTrades')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -50,6 +51,7 @@ export function OrderFilters({
|
||||
onFiltersChange,
|
||||
variant = 'inline'
|
||||
}: OrderFiltersProps) {
|
||||
const { t } = useTranslation();
|
||||
const [localFilters, setLocalFilters] = useState<P2PFilters>(filters);
|
||||
const [paymentMethods, setPaymentMethods] = useState<{ id: string; method_name: string }[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -117,7 +119,7 @@ export function OrderFilters({
|
||||
<div className="space-y-4">
|
||||
{/* Token Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Cryptocurrency</Label>
|
||||
<Label>{t('p2pFilters.cryptocurrency')}</Label>
|
||||
<div className="flex gap-2">
|
||||
{['all', 'HEZ', 'PEZ'].map((token) => (
|
||||
<Button
|
||||
@@ -127,7 +129,7 @@ export function OrderFilters({
|
||||
onClick={() => updateFilter('token', token as P2PFilters['token'])}
|
||||
className={localFilters.token === token ? 'bg-kurdish-green hover:bg-kurdish-green-dark' : ''}
|
||||
>
|
||||
{token === 'all' ? 'All' : token}
|
||||
{token === 'all' ? t('p2pFilters.all') : token}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -136,7 +138,7 @@ export function OrderFilters({
|
||||
{/* Fiat Currency */}
|
||||
<Collapsible open={expandedSections.currency} onOpenChange={() => toggleSection('currency')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Fiat Currency</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.fiatCurrency')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.currency ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
@@ -145,10 +147,10 @@ export function OrderFilters({
|
||||
onValueChange={(value) => updateFilter('fiatCurrency', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select currency" />
|
||||
<SelectValue placeholder={t('p2pFilters.selectCurrency')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Currencies</SelectItem>
|
||||
<SelectItem value="all">{t('p2pFilters.allCurrencies')}</SelectItem>
|
||||
{FIAT_CURRENCIES.map((currency) => (
|
||||
<SelectItem key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
@@ -162,7 +164,7 @@ export function OrderFilters({
|
||||
{/* Payment Methods */}
|
||||
<Collapsible open={expandedSections.payment} onOpenChange={() => toggleSection('payment')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Payment Methods</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.paymentMethods')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
{localFilters.paymentMethods.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
@@ -197,13 +199,13 @@ export function OrderFilters({
|
||||
{/* Amount Range */}
|
||||
<Collapsible open={expandedSections.amount} onOpenChange={() => toggleSection('amount')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Amount Range</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.amountRange')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.amount ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs">Min Amount</Label>
|
||||
<Label className="text-xs">{t('p2pFilters.minAmount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
@@ -212,10 +214,10 @@ export function OrderFilters({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Max Amount</Label>
|
||||
<Label className="text-xs">{t('p2pFilters.maxAmount')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="No limit"
|
||||
placeholder={t('p2pFilters.noLimit')}
|
||||
value={localFilters.maxAmount || ''}
|
||||
onChange={(e) => updateFilter('maxAmount', e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
@@ -227,7 +229,7 @@ export function OrderFilters({
|
||||
{/* Merchant Tier */}
|
||||
<Collapsible open={expandedSections.merchant} onOpenChange={() => toggleSection('merchant')}>
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full py-2">
|
||||
<Label className="cursor-pointer">Merchant Tier</Label>
|
||||
<Label className="cursor-pointer">{t('p2pFilters.merchantTier')}</Label>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSections.merchant ? 'rotate-180' : ''}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2 space-y-2">
|
||||
@@ -248,7 +250,7 @@ export function OrderFilters({
|
||||
/>
|
||||
<label htmlFor={`tier-${tier.value}`} className="flex items-center gap-1 text-sm cursor-pointer">
|
||||
<Icon className={`h-4 w-4 ${tier.color}`} />
|
||||
{tier.label}+ only
|
||||
{t(`p2pFilters.${tier.value}Plus`)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@@ -258,7 +260,7 @@ export function OrderFilters({
|
||||
|
||||
{/* Completion Rate */}
|
||||
<div className="space-y-2">
|
||||
<Label>Min Completion Rate: {localFilters.minCompletionRate}%</Label>
|
||||
<Label>{t('p2pFilters.minCompletionRate', { percent: localFilters.minCompletionRate })}</Label>
|
||||
<Slider
|
||||
value={[localFilters.minCompletionRate]}
|
||||
onValueChange={([value]) => updateFilter('minCompletionRate', value)}
|
||||
@@ -277,7 +279,7 @@ export function OrderFilters({
|
||||
onCheckedChange={(checked) => updateFilter('onlineOnly', !!checked)}
|
||||
/>
|
||||
<label htmlFor="online-only" className="text-sm cursor-pointer">
|
||||
Online traders only
|
||||
{t('p2pFilters.onlineOnly')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -288,14 +290,14 @@ export function OrderFilters({
|
||||
onCheckedChange={(checked) => updateFilter('verifiedOnly', !!checked)}
|
||||
/>
|
||||
<label htmlFor="verified-only" className="text-sm cursor-pointer">
|
||||
Verified merchants only
|
||||
{t('p2pFilters.verifiedOnly')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<div className="space-y-2">
|
||||
<Label>Sort By</Label>
|
||||
<Label>{t('p2pFilters.sortBy')}</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={localFilters.sortBy}
|
||||
@@ -305,10 +307,10 @@ export function OrderFilters({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="price">Price</SelectItem>
|
||||
<SelectItem value="completion_rate">Completion Rate</SelectItem>
|
||||
<SelectItem value="trades">Trade Count</SelectItem>
|
||||
<SelectItem value="newest">Newest</SelectItem>
|
||||
<SelectItem value="price">{t('p2pFilters.sortPrice')}</SelectItem>
|
||||
<SelectItem value="completion_rate">{t('p2pFilters.sortCompletionRate')}</SelectItem>
|
||||
<SelectItem value="trades">{t('p2pFilters.sortTradeCount')}</SelectItem>
|
||||
<SelectItem value="newest">{t('p2pFilters.sortNewest')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
@@ -319,8 +321,8 @@ export function OrderFilters({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="asc">Low to High</SelectItem>
|
||||
<SelectItem value="desc">High to Low</SelectItem>
|
||||
<SelectItem value="asc">{t('p2pFilters.lowToHigh')}</SelectItem>
|
||||
<SelectItem value="desc">{t('p2pFilters.highToLow')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -335,7 +337,7 @@ export function OrderFilters({
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
Filters
|
||||
{t('p2pFilters.filters')}
|
||||
{activeFilterCount() > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{activeFilterCount()}
|
||||
@@ -348,11 +350,11 @@ export function OrderFilters({
|
||||
<SheetTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5" />
|
||||
Filter Orders
|
||||
{t('p2pFilters.filterOrders')}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
{t('p2pFilters.reset')}
|
||||
</Button>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -361,11 +363,11 @@ export function OrderFilters({
|
||||
</div>
|
||||
<SheetFooter className="absolute bottom-0 left-0 right-0 p-4 bg-background border-t">
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)} className="flex-1">
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button onClick={applyFilters} className="flex-1 bg-kurdish-green hover:bg-kurdish-green-dark">
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Apply Filters
|
||||
{t('p2pFilters.applyFilters')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
@@ -380,17 +382,17 @@ export function OrderFilters({
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filters
|
||||
{t('p2pFilters.filters')}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
{t('p2pFilters.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
<FilterContent />
|
||||
<Button onClick={applyFilters} className="w-full mt-4 bg-kurdish-green hover:bg-kurdish-green-dark">
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Apply Filters
|
||||
{t('p2pFilters.applyFilters')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -405,6 +407,7 @@ export function QuickFilterBar({
|
||||
filters: P2PFilters;
|
||||
onFiltersChange: (filters: P2PFilters) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Token quick select */}
|
||||
@@ -417,7 +420,7 @@ export function QuickFilterBar({
|
||||
onClick={() => onFiltersChange({ ...filters, token: token as P2PFilters['token'] })}
|
||||
className={filters.token === token ? 'bg-kurdish-green hover:bg-kurdish-green-dark' : ''}
|
||||
>
|
||||
{token === 'all' ? 'All' : token}
|
||||
{token === 'all' ? t('p2pFilters.all') : token}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -428,10 +431,10 @@ export function QuickFilterBar({
|
||||
onValueChange={(value) => onFiltersChange({ ...filters, fiatCurrency: value })}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-9">
|
||||
<SelectValue placeholder="Currency" />
|
||||
<SelectValue placeholder={t('p2p.currency')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="all">{t('p2pFilters.all')}</SelectItem>
|
||||
{FIAT_CURRENCIES.map((currency) => (
|
||||
<SelectItem key={currency.value} value={currency.value}>
|
||||
{currency.value}
|
||||
@@ -443,7 +446,7 @@ export function QuickFilterBar({
|
||||
{/* Amount input */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="I want to trade..."
|
||||
placeholder={t('p2pFilters.iWantToTrade')}
|
||||
className="w-[150px] h-9"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value ? Number(e.target.value) : null;
|
||||
@@ -474,7 +477,7 @@ export function QuickFilterBar({
|
||||
|
||||
{filters.minCompletionRate > 0 && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
{filters.minCompletionRate}%+ rate
|
||||
{t('p2pFilters.rate', { percent: filters.minCompletionRate })}
|
||||
<button
|
||||
onClick={() => onFiltersChange({ ...filters, minCompletionRate: 0 })}
|
||||
className="ml-1 hover:text-destructive"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -25,6 +26,7 @@ interface UserStats {
|
||||
}
|
||||
|
||||
export function P2PDashboard() {
|
||||
const { t } = useTranslation();
|
||||
const [showCreateAd, setShowCreateAd] = useState(false);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ activeTrades: 0, completedTrades: 0, totalVolume: 0 });
|
||||
const [filters, setFilters] = useState<P2PFilters>(DEFAULT_FILTERS);
|
||||
@@ -89,7 +91,7 @@ export function P2PDashboard() {
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Back to Home
|
||||
{t('p2p.backToHome')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<NotificationBell />
|
||||
@@ -99,7 +101,7 @@ export function P2PDashboard() {
|
||||
className="border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
<Store className="w-4 h-4 mr-2" />
|
||||
Merchant
|
||||
{t('p2p.merchant')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -107,7 +109,7 @@ export function P2PDashboard() {
|
||||
className="border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
<ClipboardList className="w-4 h-4 mr-2" />
|
||||
My Trades
|
||||
{t('p2p.myTrades')}
|
||||
{userStats.activeTrades > 0 && (
|
||||
<Badge className="ml-2 bg-yellow-500 text-black">
|
||||
{userStats.activeTrades}
|
||||
@@ -138,7 +140,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.activeTrades}</p>
|
||||
<p className="text-sm text-gray-400">Active Trades</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.activeTrades')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -149,7 +151,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.completedTrades}</p>
|
||||
<p className="text-sm text-gray-400">Completed</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.completed')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -160,7 +162,7 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">${userStats.totalVolume.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400">Volume</p>
|
||||
<p className="text-sm text-gray-400">{t('p2p.volume')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -170,12 +172,12 @@ export function P2PDashboard() {
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white">P2P Trading</h1>
|
||||
<p className="text-gray-400 text-sm sm:text-base">Buy and sell crypto with your local currency.</p>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white">{t('p2p.title')}</h1>
|
||||
<p className="text-gray-400 text-sm sm:text-base">{t('p2p.subtitle')}</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateAd(true)} className="w-full sm:w-auto">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
Post a New Ad
|
||||
{t('p2p.postNewAd')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -190,14 +192,14 @@ export function P2PDashboard() {
|
||||
<TabsList className="grid w-full grid-cols-5 overflow-x-auto scrollbar-hide">
|
||||
<TabsTrigger value="express" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span className="hidden xs:inline">Express</span>
|
||||
<span className="hidden xs:inline">{t('p2p.tabExpress')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="buy" className="text-xs sm:text-sm px-1 sm:px-3">Buy</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="text-xs sm:text-sm px-1 sm:px-3">Sell</TabsTrigger>
|
||||
<TabsTrigger value="my-ads" className="text-xs sm:text-sm px-1 sm:px-3">My Ads</TabsTrigger>
|
||||
<TabsTrigger value="buy" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabBuy')}</TabsTrigger>
|
||||
<TabsTrigger value="sell" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabSell')}</TabsTrigger>
|
||||
<TabsTrigger value="my-ads" className="text-xs sm:text-sm px-1 sm:px-3">{t('p2p.tabMyAds')}</TabsTrigger>
|
||||
<TabsTrigger value="otc" className="flex items-center gap-1 text-xs sm:text-sm px-1 sm:px-3">
|
||||
<Blocks className="w-3 h-3" />
|
||||
<span className="hidden xs:inline">OTC</span>
|
||||
<span className="hidden xs:inline">{t('p2p.tabOtc')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="express">
|
||||
@@ -206,23 +208,23 @@ export function P2PDashboard() {
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Why Express Mode?</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">{t('p2p.whyExpress')}</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Instant best-rate matching
|
||||
{t('p2p.instantMatching')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Verified merchants only
|
||||
{t('p2p.verifiedMerchantsOnly')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
Escrow protection
|
||||
{t('p2p.escrowProtection')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
No manual offer selection
|
||||
{t('p2p.noManualSelection')}
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
@@ -245,27 +247,27 @@ export function P2PDashboard() {
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Block Trade Benefits</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">{t('p2p.blockTradeBenefits')}</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-400">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Custom pricing negotiation
|
||||
{t('p2p.customPricing')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Dedicated OTC desk support
|
||||
{t('p2p.dedicatedSupport')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Multi-tranche settlements
|
||||
{t('p2p.multiTranche')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Enhanced privacy
|
||||
{t('p2p.enhancedPrivacy')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-purple-400" />
|
||||
Flexible payment terms
|
||||
{t('p2p.flexiblePayment')}
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Star, Loader2, ThumbsUp, ThumbsDown } from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface RatingModalProps {
|
||||
@@ -32,6 +33,7 @@ export function RatingModal({
|
||||
counterpartyWallet,
|
||||
isBuyer,
|
||||
}: RatingModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [rating, setRating] = useState(0);
|
||||
const [hoveredRating, setHoveredRating] = useState(0);
|
||||
@@ -40,7 +42,7 @@ export function RatingModal({
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!user || rating === 0) {
|
||||
toast.error('Please select a rating');
|
||||
toast.error(t('p2pRating.selectRatingError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ export function RatingModal({
|
||||
.single();
|
||||
|
||||
if (existingRating) {
|
||||
toast.error('You have already rated this trade');
|
||||
toast.error(t('p2pRating.alreadyRated'));
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
@@ -111,11 +113,11 @@ export function RatingModal({
|
||||
is_read: false,
|
||||
});
|
||||
|
||||
toast.success('Rating submitted successfully');
|
||||
toast.success(t('p2pRating.submitted'));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Submit rating error:', error);
|
||||
toast.error('Failed to submit rating');
|
||||
toast.error(t('p2pRating.failedToSubmit'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -148,30 +150,30 @@ export function RatingModal({
|
||||
|
||||
const getRatingLabel = (r: number): string => {
|
||||
switch (r) {
|
||||
case 1: return 'Poor';
|
||||
case 2: return 'Fair';
|
||||
case 3: return 'Good';
|
||||
case 4: return 'Very Good';
|
||||
case 5: return 'Excellent';
|
||||
default: return 'Select a rating';
|
||||
case 1: return t('p2pRating.poor');
|
||||
case 2: return t('p2pRating.fair');
|
||||
case 3: return t('p2pRating.good');
|
||||
case 4: return t('p2pRating.veryGood');
|
||||
case 5: return t('p2pRating.excellent');
|
||||
default: return t('p2pRating.selectRating');
|
||||
}
|
||||
};
|
||||
|
||||
const quickReviews = [
|
||||
{ icon: ThumbsUp, text: 'Fast payment', positive: true },
|
||||
{ icon: ThumbsUp, text: 'Good communication', positive: true },
|
||||
{ icon: ThumbsUp, text: 'Smooth transaction', positive: true },
|
||||
{ icon: ThumbsDown, text: 'Slow response', positive: false },
|
||||
{ icon: ThumbsDown, text: 'Delayed payment', positive: false },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.fastPayment'), positive: true },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.goodCommunication'), positive: true },
|
||||
{ icon: ThumbsUp, text: t('p2pRating.smoothTransaction'), positive: true },
|
||||
{ icon: ThumbsDown, text: t('p2pRating.slowResponse'), positive: false },
|
||||
{ icon: ThumbsDown, text: t('p2pRating.delayedPayment'), positive: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rate Your Experience</DialogTitle>
|
||||
<DialogTitle>{t('p2pRating.title')}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
How was your trade with {counterpartyWallet.slice(0, 6)}...{counterpartyWallet.slice(-4)}?
|
||||
{t('p2pRating.description', { address: `${counterpartyWallet.slice(0, 6)}...${counterpartyWallet.slice(-4)}` })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -190,7 +192,7 @@ export function RatingModal({
|
||||
|
||||
{/* Quick Review Buttons */}
|
||||
<div>
|
||||
<Label className="text-gray-400 text-sm">Quick feedback (optional)</Label>
|
||||
<Label className="text-gray-400 text-sm">{t('p2pRating.quickFeedback')}</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{quickReviews.map((qr, i) => (
|
||||
<button
|
||||
@@ -218,13 +220,13 @@ export function RatingModal({
|
||||
{/* Review Text */}
|
||||
<div>
|
||||
<Label htmlFor="review" className="text-gray-400 text-sm">
|
||||
Additional comments (optional)
|
||||
{t('p2pRating.additionalComments')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="review"
|
||||
value={review}
|
||||
onChange={(e) => setReview(e.target.value)}
|
||||
placeholder="Share your experience..."
|
||||
placeholder={t('p2pRating.sharePlaceholder')}
|
||||
className="mt-2 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 resize-none"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
@@ -243,7 +245,7 @@ export function RatingModal({
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}
|
||||
`}>
|
||||
Rating as {isBuyer ? 'Buyer' : 'Seller'}
|
||||
{isBuyer ? t('p2pRating.ratingAsBuyer') : t('p2pRating.ratingAsSeller')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +257,7 @@ export function RatingModal({
|
||||
disabled={loading}
|
||||
className="border-gray-700"
|
||||
>
|
||||
Skip
|
||||
{t('p2pRating.skip')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -265,10 +267,10 @@ export function RatingModal({
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Submitting...
|
||||
{t('p2pRating.submitting')}
|
||||
</>
|
||||
) : (
|
||||
'Submit Rating'
|
||||
t('p2pRating.submitRating')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
interface Message {
|
||||
@@ -41,6 +42,7 @@ export function TradeChat({
|
||||
counterpartyWallet,
|
||||
isTradeActive,
|
||||
}: TradeChatProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
@@ -158,7 +160,7 @@ export function TradeChat({
|
||||
await supabase.from('p2p_notifications').insert({
|
||||
user_id: counterpartyId,
|
||||
type: 'new_message',
|
||||
title: 'New Message',
|
||||
title: t('p2pChat.newMessage'),
|
||||
message: messageText.slice(0, 100),
|
||||
reference_type: 'trade',
|
||||
reference_id: tradeId,
|
||||
@@ -166,7 +168,7 @@ export function TradeChat({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Send message error:', error);
|
||||
toast.error('Failed to send message');
|
||||
toast.error(t('p2pChat.failedToSend'));
|
||||
setNewMessage(messageText); // Restore message
|
||||
} finally {
|
||||
setSending(false);
|
||||
@@ -188,12 +190,12 @@ export function TradeChat({
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Please select an image file');
|
||||
toast.error(t('p2pChat.selectImage'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('Image must be less than 5MB');
|
||||
toast.error(t('p2pChat.imageTooLarge'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ export function TradeChat({
|
||||
const { error: msgError } = await supabase.from('p2p_messages').insert({
|
||||
trade_id: tradeId,
|
||||
sender_id: user.id,
|
||||
message: 'Sent an image',
|
||||
message: t('p2pChat.sentImage'),
|
||||
message_type: 'image',
|
||||
attachment_url: urlData.publicUrl,
|
||||
is_read: false,
|
||||
@@ -229,17 +231,17 @@ export function TradeChat({
|
||||
await supabase.from('p2p_notifications').insert({
|
||||
user_id: counterpartyId,
|
||||
type: 'new_message',
|
||||
title: 'New Image',
|
||||
message: 'Sent an image',
|
||||
title: t('p2pChat.newImage'),
|
||||
message: t('p2pChat.sentImage'),
|
||||
reference_type: 'trade',
|
||||
reference_id: tradeId,
|
||||
is_read: false,
|
||||
});
|
||||
|
||||
toast.success('Image sent');
|
||||
toast.success(t('p2pChat.imageSent'));
|
||||
} catch (error) {
|
||||
console.error('Upload image error:', error);
|
||||
toast.error('Failed to upload image');
|
||||
toast.error(t('p2pChat.failedToUpload'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
@@ -301,7 +303,7 @@ export function TradeChat({
|
||||
>
|
||||
<img
|
||||
src={message.attachment_url}
|
||||
alt="Shared image"
|
||||
alt={t('p2pChat.sharedImage')}
|
||||
className="max-w-[200px] max-h-[200px] rounded-lg"
|
||||
/>
|
||||
</a>
|
||||
@@ -329,7 +331,7 @@ export function TradeChat({
|
||||
<Card className="bg-gray-900 border-gray-800 h-[400px] flex flex-col">
|
||||
<CardHeader className="py-3 px-4 border-b border-gray-800">
|
||||
<CardTitle className="text-white text-base flex items-center gap-2">
|
||||
<span>Chat</span>
|
||||
<span>{t('p2pChat.title')}</span>
|
||||
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length > 0 && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-green-500 text-white rounded-full">
|
||||
{messages.filter(m => m.sender_id !== user?.id && !m.is_read).length}
|
||||
@@ -348,8 +350,8 @@ export function TradeChat({
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<AlertCircle className="w-8 h-8 mb-2" />
|
||||
<p className="text-sm">No messages yet</p>
|
||||
<p className="text-xs">Start the conversation</p>
|
||||
<p className="text-sm">{t('p2pChat.noMessages')}</p>
|
||||
<p className="text-xs">{t('p2pChat.startConversation')}</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map(renderMessage)
|
||||
@@ -384,7 +386,7 @@ export function TradeChat({
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message..."
|
||||
placeholder={t('p2pChat.placeholder')}
|
||||
disabled={sending}
|
||||
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
|
||||
/>
|
||||
@@ -405,7 +407,7 @@ export function TradeChat({
|
||||
) : (
|
||||
<div className="p-3 border-t border-gray-800 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Chat is disabled for completed/cancelled trades
|
||||
{t('p2pChat.chatDisabled')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -24,6 +25,7 @@ interface TradeModalProps {
|
||||
}
|
||||
|
||||
export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { api, selectedAccount } = usePezkuwi();
|
||||
@@ -40,28 +42,28 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
|
||||
const handleInitiateTrade = async () => {
|
||||
if (!api || !selectedAccount || !user) {
|
||||
toast.error('Please connect your wallet and log in');
|
||||
toast.error(t('p2p.connectWalletAndLogin'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent self-trading
|
||||
if (offer.seller_id === user.id) {
|
||||
toast.error('You cannot trade with your own offer');
|
||||
toast.error(t('p2pTrade.cannotTradeOwn'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidAmount) {
|
||||
toast.error('Invalid amount');
|
||||
toast.error(t('p2pTrade.invalidAmount'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!meetsMinOrder) {
|
||||
toast.error(`Minimum order: ${offer.min_order_amount} ${offer.token}`);
|
||||
toast.error(t('p2p.minOrder', { amount: offer.min_order_amount, token: offer.token }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!meetsMaxOrder) {
|
||||
toast.error(`Maximum order: ${offer.max_order_amount} ${offer.token}`);
|
||||
toast.error(t('p2p.maxOrder', { amount: offer.max_order_amount, token: offer.token }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
amount: cryptoAmount
|
||||
});
|
||||
|
||||
toast.success('Trade initiated! Proceed to payment.');
|
||||
toast.success(t('p2pTrade.tradeInitiated'));
|
||||
onClose();
|
||||
|
||||
// Navigate to trade page
|
||||
@@ -92,9 +94,9 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Buy {offer.token}</DialogTitle>
|
||||
<DialogTitle>{t('p2pTrade.buyToken', { token: offer.token })}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Trading with {offer.seller_wallet.slice(0, 6)}...{offer.seller_wallet.slice(-4)}
|
||||
{t('p2pTrade.tradingWith', { address: `${offer.seller_wallet.slice(0, 6)}...${offer.seller_wallet.slice(-4)}` })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -102,37 +104,37 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{/* Price Info */}
|
||||
<div className="p-4 bg-gray-800 rounded-lg">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-400">Price</span>
|
||||
<span className="text-gray-400">{t('p2p.price')}</span>
|
||||
<span className="text-xl font-bold text-green-400">
|
||||
{offer.price_per_unit.toFixed(2)} {offer.fiat_currency}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Available</span>
|
||||
<span className="text-gray-400">{t('p2p.available')}</span>
|
||||
<span className="text-white">{offer.remaining_amount} {offer.token}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="buyAmount">Amount to Buy ({offer.token})</Label>
|
||||
<Label htmlFor="buyAmount">{t('p2pTrade.amountToBuy', { token: offer.token })}</Label>
|
||||
<Input
|
||||
id="buyAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="Amount"
|
||||
placeholder={t('p2p.amount')}
|
||||
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
{offer.min_order_amount && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Min: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minLimit', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
{offer.max_order_amount && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Max: {offer.max_order_amount} {offer.token}
|
||||
{t('p2p.maxLimit', { amount: offer.max_order_amount, token: offer.token })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -140,7 +142,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{/* Calculation */}
|
||||
{cryptoAmount > 0 && (
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-sm text-gray-400 mb-1">You will pay</p>
|
||||
<p className="text-sm text-gray-400 mb-1">{t('p2pTrade.youWillPay')}</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
{fiatAmount.toFixed(2)} {offer.fiat_currency}
|
||||
</p>
|
||||
@@ -152,7 +154,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Minimum order: {offer.min_order_amount} {offer.token}
|
||||
{t('p2p.minOrder', { amount: offer.min_order_amount, token: offer.token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -161,7 +163,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Maximum order: {offer.max_order_amount} {offer.token}
|
||||
{t('p2p.maxOrder', { amount: offer.max_order_amount, token: offer.token })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -170,7 +172,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
<Alert>
|
||||
<Clock className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Payment deadline: {offer.time_limit_minutes} minutes after accepting
|
||||
{t('p2pTrade.paymentDeadline', { minutes: offer.time_limit_minutes })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -182,7 +184,7 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
disabled={loading}
|
||||
className="bg-gray-800 border-gray-700 hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
{t('p2p.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInitiateTrade}
|
||||
@@ -191,10 +193,10 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Initiating...
|
||||
{t('p2pTrade.initiating')}
|
||||
</>
|
||||
) : (
|
||||
'Accept & Continue'
|
||||
t('p2pTrade.acceptAndContinue')
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -48,6 +49,7 @@ interface WithdrawModalProps {
|
||||
type WithdrawStep = 'form' | 'confirm' | 'success';
|
||||
|
||||
export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedAccount } = usePezkuwi();
|
||||
|
||||
const [step, setStep] = useState<WithdrawStep>('form');
|
||||
@@ -134,25 +136,25 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
const withdrawAmount = parseFloat(amount);
|
||||
|
||||
if (isNaN(withdrawAmount) || withdrawAmount <= 0) {
|
||||
return 'Please enter a valid amount';
|
||||
return t('p2pWithdraw.enterValidAmount');
|
||||
}
|
||||
|
||||
if (withdrawAmount < MIN_WITHDRAWAL) {
|
||||
return `Minimum withdrawal is ${MIN_WITHDRAWAL} ${token}`;
|
||||
return t('p2pWithdraw.minimumWithdrawal', { amount: MIN_WITHDRAWAL, token });
|
||||
}
|
||||
|
||||
if (withdrawAmount > getMaxWithdrawable()) {
|
||||
return 'Insufficient available balance';
|
||||
return t('p2pWithdraw.insufficientBalance');
|
||||
}
|
||||
|
||||
if (!walletAddress || walletAddress.length < 40) {
|
||||
return 'Please enter a valid wallet address';
|
||||
return t('p2pWithdraw.invalidAddress');
|
||||
}
|
||||
|
||||
// Check for pending requests
|
||||
const hasPendingForToken = pendingRequests.some(r => r.token === token);
|
||||
if (hasPendingForToken) {
|
||||
return `You already have a pending ${token} withdrawal request`;
|
||||
return t('p2pWithdraw.pendingForToken', { token });
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -201,14 +203,14 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<>
|
||||
{/* Token Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Label>{t('p2pWithdraw.selectToken')}</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
<SelectItem value="HEZ">{t('p2pWithdraw.hezNative')}</SelectItem>
|
||||
<SelectItem value="PEZ">{t('p2pWithdraw.pez')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -217,13 +219,13 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Available</p>
|
||||
<p className="text-muted-foreground">{t('p2pWithdraw.available')}</p>
|
||||
<p className="font-semibold text-green-500">
|
||||
{getAvailableBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Locked (Escrow)</p>
|
||||
<p className="text-muted-foreground">{t('p2pWithdraw.lockedEscrow')}</p>
|
||||
<p className="font-semibold text-yellow-500">
|
||||
{getLockedBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
@@ -233,7 +235,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<Label>Withdrawal Amount</Label>
|
||||
<Label>{t('p2pWithdraw.withdrawalAmount')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -253,17 +255,17 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 h-7 text-xs"
|
||||
onClick={handleSetMax}
|
||||
>
|
||||
MAX
|
||||
{t('p2pWithdraw.max')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Min: {MIN_WITHDRAWAL} {token} | Max: {getMaxWithdrawable().toFixed(4)} {token}
|
||||
{t('p2pWithdraw.minMax', { min: MIN_WITHDRAWAL, max: getMaxWithdrawable().toFixed(4), token })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Wallet Address */}
|
||||
<div className="space-y-2">
|
||||
<Label>Destination Wallet Address</Label>
|
||||
<Label>{t('p2pWithdraw.destinationAddress')}</Label>
|
||||
<Input
|
||||
value={walletAddress}
|
||||
onChange={(e) => setWalletAddress(e.target.value)}
|
||||
@@ -271,7 +273,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only PezkuwiChain addresses are supported
|
||||
{t('p2pWithdraw.onlyPezkuwiAddresses')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -280,7 +282,7 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Network fee: ~{NETWORK_FEE} HEZ (deducted from withdrawal amount)
|
||||
{t('p2pWithdraw.networkFee', { fee: NETWORK_FEE })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -290,21 +292,20 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You have {pendingRequests.length} pending withdrawal request(s).
|
||||
Please wait for them to complete.
|
||||
{t('p2pWithdraw.pendingWarning', { count: pendingRequests.length })}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
{t('continue')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@@ -321,28 +322,27 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please review your withdrawal details carefully.
|
||||
This action cannot be undone.
|
||||
{t('p2pWithdraw.reviewWarning')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Token</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.tokenLabel')}</span>
|
||||
<span className="font-semibold">{token}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Withdrawal Amount</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.withdrawalAmountLabel')}</span>
|
||||
<span className="font-semibold">{withdrawAmount.toFixed(4)} {token}</span>
|
||||
</div>
|
||||
{token === 'HEZ' && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Network Fee</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.networkFeeLabel')}</span>
|
||||
<span className="text-yellow-500">-{NETWORK_FEE} HEZ</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t pt-4 flex justify-between items-center">
|
||||
<span className="text-muted-foreground">You Will Receive</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.youWillReceive')}</span>
|
||||
<span className="font-bold text-lg text-green-500">
|
||||
{receiveAmount.toFixed(4)} {token}
|
||||
</span>
|
||||
@@ -350,18 +350,18 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/30 border">
|
||||
<p className="text-xs text-muted-foreground mb-1">Destination Address</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">{t('p2pWithdraw.destinationAddressLabel')}</p>
|
||||
<p className="font-mono text-xs break-all">{walletAddress}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Processing time: Usually within 5-30 minutes</span>
|
||||
<span>{t('p2pWithdraw.processingTime')}</span>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setStep('form')}>
|
||||
Back
|
||||
{t('back')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitWithdrawal}
|
||||
@@ -370,12 +370,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
{t('p2pWithdraw.processing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Confirm Withdrawal
|
||||
{t('p2pWithdraw.confirmWithdrawal')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -392,29 +392,29 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Withdrawal Request Submitted!
|
||||
{t('p2pWithdraw.requestSubmitted')}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Your withdrawal request has been submitted for processing.
|
||||
{t('p2pWithdraw.requestSubmittedDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Request ID</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.requestId')}</span>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{requestId.slice(0, 8)}...
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.statusLabel')}</span>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Processing
|
||||
{t('p2pWithdraw.statusProcessing')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Amount</span>
|
||||
<span className="text-muted-foreground">{t('p2pWithdraw.amountLabel')}</span>
|
||||
<span className="font-semibold">{amount} {token}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,13 +422,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You can track your withdrawal status in the transaction history.
|
||||
Funds will arrive in your wallet within 5-30 minutes.
|
||||
{t('p2pWithdraw.trackInfo')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
{t('p2pWithdraw.done')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -439,12 +438,12 @@ export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ArrowUpFromLine className="h-5 w-5" />
|
||||
Withdraw from P2P Balance
|
||||
{t('p2pWithdraw.title')}
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'form' && 'Withdraw crypto from your P2P balance to external wallet'}
|
||||
{step === 'confirm' && 'Review and confirm your withdrawal'}
|
||||
{step === 'form' && t('p2pWithdraw.formStep')}
|
||||
{step === 'confirm' && t('p2pWithdraw.confirmStep')}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
Reference in New Issue
Block a user