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:
2026-02-22 04:48:20 +03:00
parent 5b26cc8907
commit 4f683538d3
129 changed files with 22442 additions and 4186 deletions
+17 -15
View File
@@ -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>
+31 -30
View File
@@ -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>
+39 -37
View File
@@ -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>
+45 -46
View File
@@ -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>
+44 -44
View File
@@ -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>
+27 -25
View File
@@ -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>
+13 -11
View File
@@ -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>
+32 -32
View File
@@ -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>
+16 -10
View File
@@ -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>
+10 -8
View File
@@ -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>
</>
)}
+38 -35
View File
@@ -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"
+27 -25
View File
@@ -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>
+26 -24
View File
@@ -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>
+17 -15
View File
@@ -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>
)}
+23 -21
View File
@@ -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>
+42 -43
View File
@@ -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>