fix: resolve ESLint/Prettier issues in P2P components

- Fix prettier formatting across all P2P files
- Fix setState-in-useEffect by using useCallback pattern
- Add missing React import for keyboard event type
- Wrap fetch functions in useCallback for exhaustive-deps
This commit is contained in:
2026-02-26 19:03:59 +03:00
parent 31e768de45
commit 0b72cc4a4d
12 changed files with 322 additions and 188 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.193", "version": "1.0.221",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.193", "version": "1.0.221",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pezkuwi/api": "^16.5.36", "@pezkuwi/api": "^16.5.36",
@@ -53,7 +53,7 @@
"prettier": "^3.8.1", "prettier": "^3.8.1",
"tailwindcss": "^3.4.11", "tailwindcss": "^3.4.11",
"tronweb": "^6.2.0", "tronweb": "^6.2.0",
"typescript": "^5.5.3", "typescript": "^5.9.3",
"vite": "^5.4.1", "vite": "^5.4.1",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.224", "version": "1.0.225",
"type": "module", "type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team", "author": "Pezkuwichain Team",
@@ -81,7 +81,7 @@
"prettier": "^3.8.1", "prettier": "^3.8.1",
"tailwindcss": "^3.4.11", "tailwindcss": "^3.4.11",
"tronweb": "^6.2.0", "tronweb": "^6.2.0",
"typescript": "^5.5.3", "typescript": "^5.9.3",
"vite": "^5.4.1", "vite": "^5.4.1",
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
+7 -5
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Wallet, Lock, RefreshCw } from 'lucide-react'; import { Wallet, Lock, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
@@ -19,7 +19,7 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const fetchBalances = async () => { const fetchBalances = useCallback(async () => {
if (!sessionToken) return; if (!sessionToken) return;
try { try {
const data = await getInternalBalance(sessionToken); const data = await getInternalBalance(sessionToken);
@@ -30,11 +30,11 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
}; }, [sessionToken]);
useEffect(() => { useEffect(() => {
fetchBalances(); fetchBalances();
}, [sessionToken]); }, [fetchBalances]);
const handleRefresh = () => { const handleRefresh = () => {
hapticImpact('light'); hapticImpact('light');
@@ -65,7 +65,9 @@ export function BalanceCard({ onRefresh }: BalanceCardProps) {
disabled={refreshing} disabled={refreshing}
className="p-1.5 rounded-full hover:bg-muted/50 transition-colors" className="p-1.5 rounded-full hover:bg-muted/50 transition-colors"
> >
<RefreshCw className={cn('w-4 h-4 text-muted-foreground', refreshing && 'animate-spin')} /> <RefreshCw
className={cn('w-4 h-4 text-muted-foreground', refreshing && 'animate-spin')}
/>
</button> </button>
</div> </div>
+40 -15
View File
@@ -4,7 +4,12 @@ import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram'; import { useTelegram } from '@/hooks/useTelegram';
import { createP2POffer, getPaymentMethods, getInternalBalance, type PaymentMethod } from '@/lib/p2p-api'; import {
createP2POffer,
getPaymentMethods,
getInternalBalance,
type PaymentMethod,
} from '@/lib/p2p-api';
interface CreateOfferModalProps { interface CreateOfferModalProps {
isOpen: boolean; isOpen: boolean;
@@ -39,9 +44,7 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
useEffect(() => { useEffect(() => {
if (!isOpen || !sessionToken) return; if (!isOpen || !sessionToken) return;
getPaymentMethods(sessionToken, fiatCurrency) getPaymentMethods(sessionToken, fiatCurrency).then(setPaymentMethods).catch(console.error);
.then(setPaymentMethods)
.catch(console.error);
}, [isOpen, sessionToken, fiatCurrency]); }, [isOpen, sessionToken, fiatCurrency]);
useEffect(() => { useEffect(() => {
@@ -148,19 +151,25 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border" className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
> >
{TOKENS.map((tk) => ( {TOKENS.map((tk) => (
<option key={tk} value={tk}>{tk}</option> <option key={tk} value={tk}>
{tk}
</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.fiatCurrency')}</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.fiatCurrency')}
</label>
<select <select
value={fiatCurrency} value={fiatCurrency}
onChange={(e) => setFiatCurrency(e.target.value)} onChange={(e) => setFiatCurrency(e.target.value)}
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border" className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
> >
{FIAT_CURRENCIES.map((c) => ( {FIAT_CURRENCIES.map((c) => (
<option key={c} value={c}>{c}</option> <option key={c} value={c}>
{c}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -188,7 +197,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
{/* Fiat Amount */} {/* Fiat Amount */}
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.fiatTotal')} ({fiatCurrency})</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.fiatTotal')} ({fiatCurrency})
</label>
<input <input
type="number" type="number"
value={fiatAmount} value={fiatAmount}
@@ -199,14 +210,18 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
/> />
{pricePerUnit > 0 && ( {pricePerUnit > 0 && (
<p className="text-xs text-cyan-400 mt-1"> <p className="text-xs text-cyan-400 mt-1">
{t('p2p.pricePerUnit')}: {pricePerUnit.toLocaleString(undefined, { maximumFractionDigits: 2 })} {fiatCurrency}/{token} {t('p2p.pricePerUnit')}:{' '}
{pricePerUnit.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
{fiatCurrency}/{token}
</p> </p>
)} )}
</div> </div>
{/* Payment Method */} {/* Payment Method */}
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.paymentMethod')}</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.paymentMethod')}
</label>
<select <select
value={paymentMethodId} value={paymentMethodId}
onChange={(e) => setPaymentMethodId(e.target.value)} onChange={(e) => setPaymentMethodId(e.target.value)}
@@ -214,14 +229,18 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
> >
<option value="">{t('p2p.selectPaymentMethod')}</option> <option value="">{t('p2p.selectPaymentMethod')}</option>
{paymentMethods.map((pm) => ( {paymentMethods.map((pm) => (
<option key={pm.id} value={pm.id}>{pm.method_name}</option> <option key={pm.id} value={pm.id}>
{pm.method_name}
</option>
))} ))}
</select> </select>
</div> </div>
{/* Payment Details */} {/* Payment Details */}
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.paymentDetails')}</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.paymentDetails')}
</label>
<textarea <textarea
value={paymentDetails} value={paymentDetails}
onChange={(e) => setPaymentDetails(e.target.value)} onChange={(e) => setPaymentDetails(e.target.value)}
@@ -234,7 +253,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
{/* Order Limits */} {/* Order Limits */}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.minOrder')}</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.minOrder')}
</label>
<input <input
type="number" type="number"
value={minOrder} value={minOrder}
@@ -245,7 +266,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
/> />
</div> </div>
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">{t('p2p.maxOrder')}</label> <label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.maxOrder')}
</label>
<input <input
type="number" type="number"
value={maxOrder} value={maxOrder}
@@ -266,7 +289,9 @@ export function CreateOfferModal({ isOpen, onClose, onOfferCreated }: CreateOffe
className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border" className="w-full bg-muted rounded-xl px-3 py-2.5 text-sm text-foreground border border-border"
> >
{TIME_LIMITS.map((tl) => ( {TIME_LIMITS.map((tl) => (
<option key={tl} value={tl}>{tl} {t('p2p.minutes')}</option> <option key={tl} value={tl}>
{tl} {t('p2p.minutes')}
</option>
))} ))}
</select> </select>
</div> </div>
+9 -3
View File
@@ -81,7 +81,9 @@ export function DisputeModal({ isOpen, onClose, tradeId, onDisputeOpened }: Disp
{/* Category */} {/* Category */}
<div> <div>
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.disputeCategory')}</label> <label className="text-sm text-muted-foreground mb-1 block">
{t('p2p.disputeCategory')}
</label>
<select <select
value={category} value={category}
onChange={(e) => setCategory(e.target.value)} onChange={(e) => setCategory(e.target.value)}
@@ -89,14 +91,18 @@ export function DisputeModal({ isOpen, onClose, tradeId, onDisputeOpened }: Disp
> >
<option value="">{t('p2p.selectCategory')}</option> <option value="">{t('p2p.selectCategory')}</option>
{DISPUTE_CATEGORIES.map((cat) => ( {DISPUTE_CATEGORIES.map((cat) => (
<option key={cat} value={cat}>{t(`p2p.dispute.${cat}`)}</option> <option key={cat} value={cat}>
{t(`p2p.dispute.${cat}`)}
</option>
))} ))}
</select> </select>
</div> </div>
{/* Reason */} {/* Reason */}
<div> <div>
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.disputeReason')}</label> <label className="text-sm text-muted-foreground mb-1 block">
{t('p2p.disputeReason')}
</label>
<textarea <textarea
value={reason} value={reason}
onChange={(e) => setReason(e.target.value)} onChange={(e) => setReason(e.target.value)}
+38 -31
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Star, Clock, ChevronDown, Loader2 } from 'lucide-react'; import { Star, Clock, ChevronDown, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
@@ -29,31 +29,34 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
const limit = 20; const limit = 20;
const fetchOffers = async (p = 1) => { const fetchOffers = useCallback(
if (!sessionToken) return; async (p = 1) => {
setLoading(true); if (!sessionToken) return;
try { setLoading(true);
const result = await getP2POffers({ try {
sessionToken, const result = await getP2POffers({
adType, sessionToken,
token: selectedToken || undefined, adType,
fiatCurrency: selectedCurrency || undefined, token: selectedToken || undefined,
page: p, fiatCurrency: selectedCurrency || undefined,
limit, page: p,
}); limit,
setOffers(result.offers); });
setTotal(result.total); setOffers(result.offers);
setPage(p); setTotal(result.total);
} catch (err) { setPage(p);
console.error('Failed to fetch offers:', err); } catch (err) {
} finally { console.error('Failed to fetch offers:', err);
setLoading(false); } finally {
} setLoading(false);
}; }
},
[sessionToken, adType, selectedToken, selectedCurrency]
);
useEffect(() => { useEffect(() => {
fetchOffers(1); fetchOffers(1);
}, [sessionToken, adType, selectedCurrency, selectedToken]); }, [fetchOffers]);
const handleAccept = (offer: P2POffer) => { const handleAccept = (offer: P2POffer) => {
hapticImpact('medium'); hapticImpact('medium');
@@ -82,7 +85,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
> >
<option value="">{t('p2p.allTokens')}</option> <option value="">{t('p2p.allTokens')}</option>
{TOKENS.map((tk) => ( {TOKENS.map((tk) => (
<option key={tk} value={tk}>{tk}</option> <option key={tk} value={tk}>
{tk}
</option>
))} ))}
</select> </select>
<select <select
@@ -92,7 +97,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
> >
<option value="">{t('p2p.allCurrencies')}</option> <option value="">{t('p2p.allCurrencies')}</option>
{FIAT_CURRENCIES.map((c) => ( {FIAT_CURRENCIES.map((c) => (
<option key={c} value={c}>{c}</option> <option key={c} value={c}>
{c}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -110,10 +117,7 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{offers.map((offer) => ( {offers.map((offer) => (
<div <div key={offer.id} className="bg-card rounded-xl border border-border p-3 space-y-2">
key={offer.id}
className="bg-card rounded-xl border border-border p-3 space-y-2"
>
{/* Top: Token + Price */} {/* Top: Token + Price */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -121,7 +125,8 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
<span className="text-xs text-muted-foreground">/ {offer.fiat_currency}</span> <span className="text-xs text-muted-foreground">/ {offer.fiat_currency}</span>
</div> </div>
<span className="text-sm font-bold text-cyan-400"> <span className="text-sm font-bold text-cyan-400">
{offer.price_per_unit?.toLocaleString(undefined, { maximumFractionDigits: 2 })} {offer.fiat_currency} {offer.price_per_unit?.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
{offer.fiat_currency}
</span> </span>
</div> </div>
@@ -179,7 +184,9 @@ export function OfferList({ adType, onAcceptOffer }: OfferListProps) {
> >
{t('p2p.prev')} {t('p2p.prev')}
</button> </button>
<span className="text-xs text-muted-foreground">{page} / {totalPages}</span> <span className="text-xs text-muted-foreground">
{page} / {totalPages}
</span>
<button <button
onClick={() => fetchOffers(page + 1)} onClick={() => fetchOffers(page + 1)}
disabled={page >= totalPages} disabled={page >= totalPages}
+30 -19
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { X, Send } from 'lucide-react'; import { X, Send } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
@@ -20,11 +20,12 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
const [newMessage, setNewMessage] = useState(''); const [newMessage, setNewMessage] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
// eslint-disable-next-line no-undef
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const userId = user?.id; const userId = user?.id;
const fetchMessages = async () => { const fetchMessages = useCallback(async () => {
if (!sessionToken) return; if (!sessionToken) return;
try { try {
const msgs = await getTradeMessages(sessionToken, tradeId); const msgs = await getTradeMessages(sessionToken, tradeId);
@@ -34,13 +35,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [sessionToken, tradeId]);
useEffect(() => { useEffect(() => {
fetchMessages(); fetchMessages();
const interval = setInterval(fetchMessages, 5000); const interval = setInterval(fetchMessages, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [sessionToken, tradeId]); }, [fetchMessages]);
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -71,10 +72,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
return ( return (
<div className="fixed inset-0 z-50 flex flex-col bg-background"> <div className="fixed inset-0 z-50 flex flex-col bg-background">
{/* Header */} {/* Header */}
<div className={cn( <div
'flex items-center justify-between p-4 border-b border-border safe-area-top', className={cn(
isRTL && 'direction-rtl' 'flex items-center justify-between p-4 border-b border-border safe-area-top',
)} dir={isRTL ? 'rtl' : 'ltr'}> isRTL && 'direction-rtl'
)}
dir={isRTL ? 'rtl' : 'ltr'}
>
<h2 className="text-lg font-semibold text-foreground">{t('p2p.chat')}</h2> <h2 className="text-lg font-semibold text-foreground">{t('p2p.chat')}</h2>
<button onClick={onClose} className="p-2 rounded-full hover:bg-muted transition-colors"> <button onClick={onClose} className="p-2 rounded-full hover:bg-muted transition-colors">
<X className="w-5 h-5 text-muted-foreground" /> <X className="w-5 h-5 text-muted-foreground" />
@@ -100,8 +104,7 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
key={msg.id} key={msg.id}
className={cn( className={cn(
'flex', 'flex',
isSystem ? 'justify-center' : isSystem ? 'justify-center' : isOwn ? 'justify-end' : 'justify-start'
isOwn ? 'justify-end' : 'justify-start'
)} )}
> >
{isSystem ? ( {isSystem ? (
@@ -118,11 +121,16 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
)} )}
> >
<p className="text-sm break-words">{msg.message}</p> <p className="text-sm break-words">{msg.message}</p>
<p className={cn( <p
'text-[10px] mt-1', className={cn(
isOwn ? 'text-white/60' : 'text-muted-foreground' 'text-[10px] mt-1',
)}> isOwn ? 'text-white/60' : 'text-muted-foreground'
{new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} )}
>
{new Date(msg.created_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p> </p>
</div> </div>
)} )}
@@ -134,10 +142,13 @@ export function TradeChat({ tradeId, onClose }: TradeChatProps) {
</div> </div>
{/* Input */} {/* Input */}
<div className={cn( <div
'flex items-center gap-2 p-4 border-t border-border safe-area-bottom', className={cn(
isRTL && 'direction-rtl' 'flex items-center gap-2 p-4 border-t border-border safe-area-bottom',
)} dir={isRTL ? 'rtl' : 'ltr'}> isRTL && 'direction-rtl'
)}
dir={isRTL ? 'rtl' : 'ltr'}
>
<input <input
type="text" type="text"
value={newMessage} value={newMessage}
+6 -5
View File
@@ -27,9 +27,7 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
if (!isOpen || !offer) return null; if (!isOpen || !offer) return null;
const numAmount = parseFloat(amount) || 0; const numAmount = parseFloat(amount) || 0;
const fiatTotal = numAmount > 0 && offer.price_per_unit const fiatTotal = numAmount > 0 && offer.price_per_unit ? numAmount * offer.price_per_unit : 0;
? numAmount * offer.price_per_unit
: 0;
const isValid = const isValid =
numAmount > 0 && numAmount > 0 &&
@@ -108,7 +106,9 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
{/* Amount Input */} {/* Amount Input */}
<div> <div>
<label className="text-sm text-muted-foreground mb-1 block">{t('p2p.amount')} ({offer.token})</label> <label className="text-sm text-muted-foreground mb-1 block">
{t('p2p.amount')} ({offer.token})
</label>
<input <input
type="number" type="number"
value={amount} value={amount}
@@ -133,7 +133,8 @@ export function TradeModal({ isOpen, onClose, offer, onTradeCreated }: TradeModa
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-sm text-muted-foreground">{t('p2p.fiatTotal')}</span> <span className="text-sm text-muted-foreground">{t('p2p.fiatTotal')}</span>
<span className="text-lg font-bold text-cyan-400"> <span className="text-lg font-bold text-cyan-400">
{fiatTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })} {offer.fiat_currency} {fiatTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}{' '}
{offer.fiat_currency}
</span> </span>
</div> </div>
</div> </div>
+49 -25
View File
@@ -1,5 +1,12 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { ArrowLeft, Clock, CheckCircle2, XCircle, AlertTriangle, MessageCircle } from 'lucide-react'; import {
ArrowLeft,
Clock,
CheckCircle2,
XCircle,
AlertTriangle,
MessageCircle,
} from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
@@ -67,13 +74,23 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
const [timeRemaining, setTimeRemaining] = useState(''); const [timeRemaining, setTimeRemaining] = useState('');
useEffect(() => { useEffect(() => {
if (!trade) return; if (!trade) return;
const deadline = trade.status === 'pending' ? trade.payment_deadline : const deadline =
trade.status === 'payment_sent' ? trade.confirmation_deadline : null; trade.status === 'pending'
if (!deadline) { setTimeRemaining(''); return; } ? trade.payment_deadline
: trade.status === 'payment_sent'
? trade.confirmation_deadline
: null;
if (!deadline) {
setTimeRemaining('');
return;
}
const update = () => { const update = () => {
const diff = new Date(deadline).getTime() - Date.now(); const diff = new Date(deadline).getTime() - Date.now();
if (diff <= 0) { setTimeRemaining(t('p2p.expired')); return; } if (diff <= 0) {
setTimeRemaining(t('p2p.expired'));
return;
}
const mins = Math.floor(diff / 60000); const mins = Math.floor(diff / 60000);
const secs = Math.floor((diff % 60000) / 1000); const secs = Math.floor((diff % 60000) / 1000);
setTimeRemaining(`${mins}:${secs.toString().padStart(2, '0')}`); setTimeRemaining(`${mins}:${secs.toString().padStart(2, '0')}`);
@@ -146,7 +163,9 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
return ( return (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-sm text-muted-foreground">{t('p2p.tradeNotFound')}</p> <p className="text-sm text-muted-foreground">{t('p2p.tradeNotFound')}</p>
<button onClick={onBack} className="text-cyan-400 text-sm mt-2">{t('common.back')}</button> <button onClick={onBack} className="text-cyan-400 text-sm mt-2">
{t('common.back')}
</button>
</div> </div>
); );
} }
@@ -165,7 +184,12 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
</div> </div>
{/* Status Banner */} {/* Status Banner */}
<div className={cn('flex items-center gap-2 p-3 rounded-xl', `${statusConfig.color} bg-current/10`)}> <div
className={cn(
'flex items-center gap-2 p-3 rounded-xl',
`${statusConfig.color} bg-current/10`
)}
>
<StatusIcon className={cn('w-5 h-5', statusConfig.color)} /> <StatusIcon className={cn('w-5 h-5', statusConfig.color)} />
<span className={cn('text-sm font-medium', statusConfig.color)}> <span className={cn('text-sm font-medium', statusConfig.color)}>
{t(`p2p.status.${trade.status}`)} {t(`p2p.status.${trade.status}`)}
@@ -208,11 +232,7 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
{/* Timeline */} {/* Timeline */}
<div className="bg-card rounded-xl border border-border p-4 space-y-3"> <div className="bg-card rounded-xl border border-border p-4 space-y-3">
<h3 className="text-sm font-medium text-foreground">{t('p2p.timeline')}</h3> <h3 className="text-sm font-medium text-foreground">{t('p2p.timeline')}</h3>
<TimelineStep <TimelineStep done label={t('p2p.tradeCreated')} time={trade.created_at} />
done
label={t('p2p.tradeCreated')}
time={trade.created_at}
/>
<TimelineStep <TimelineStep
done={!!trade.buyer_marked_paid_at} done={!!trade.buyer_marked_paid_at}
active={trade.status === 'pending'} active={trade.status === 'pending'}
@@ -297,23 +317,29 @@ export function TradeView({ tradeId, onBack }: TradeViewProps) {
</div> </div>
{/* Chat Modal */} {/* Chat Modal */}
{showChat && ( {showChat && <TradeChat tradeId={trade.id} onClose={() => setShowChat(false)} />}
<TradeChat tradeId={trade.id} onClose={() => setShowChat(false)} />
)}
{/* Dispute Modal */} {/* Dispute Modal */}
<DisputeModal <DisputeModal
isOpen={showDispute} isOpen={showDispute}
onClose={() => setShowDispute(false)} onClose={() => setShowDispute(false)}
tradeId={trade.id} tradeId={trade.id}
onDisputeOpened={() => { setShowDispute(false); fetchTrade(); }} onDisputeOpened={() => {
setShowDispute(false);
fetchTrade();
}}
/> />
</div> </div>
); );
} }
// Timeline step sub-component // Timeline step sub-component
function TimelineStep({ done, active, label, time }: { function TimelineStep({
done,
active,
label,
time,
}: {
done?: boolean; done?: boolean;
active?: boolean; active?: boolean;
label: string; label: string;
@@ -324,18 +350,16 @@ function TimelineStep({ done, active, label, time }: {
<div <div
className={cn( className={cn(
'w-3 h-3 rounded-full border-2 flex-shrink-0', 'w-3 h-3 rounded-full border-2 flex-shrink-0',
done ? 'bg-green-400 border-green-400' : done
active ? 'border-cyan-400 animate-pulse' : ? 'bg-green-400 border-green-400'
'border-muted-foreground/30' : active
? 'border-cyan-400 animate-pulse'
: 'border-muted-foreground/30'
)} )}
/> />
<div className="flex-1"> <div className="flex-1">
<p className={cn('text-sm', done ? 'text-foreground' : 'text-muted-foreground')}>{label}</p> <p className={cn('text-sm', done ? 'text-foreground' : 'text-muted-foreground')}>{label}</p>
{time && ( {time && <p className="text-xs text-muted-foreground">{new Date(time).toLocaleString()}</p>}
<p className="text-xs text-muted-foreground">
{new Date(time).toLocaleString()}
</p>
)}
</div> </div>
</div> </div>
); );
+68 -40
View File
@@ -126,9 +126,7 @@ async function callEdgeFunction<T>(
// ─── Balance ───────────────────────────────────────────────── // ─── Balance ─────────────────────────────────────────────────
export async function getInternalBalance( export async function getInternalBalance(sessionToken: string): Promise<InternalBalance[]> {
sessionToken: string
): Promise<InternalBalance[]> {
const result = await callEdgeFunction<{ success: boolean; balances: InternalBalance[] }>( const result = await callEdgeFunction<{ success: boolean; balances: InternalBalance[] }>(
'get-internal-balance', 'get-internal-balance',
{ sessionToken } { sessionToken }
@@ -172,14 +170,18 @@ export async function getP2POffers(params: GetOffersParams): Promise<GetOffersRe
'get-p2p-offers', 'get-p2p-offers',
params as unknown as Record<string, unknown> params as unknown as Record<string, unknown>
); );
return { offers: result.offers || [], total: result.total || 0, page: result.page || 1, limit: result.limit || 20 }; return {
offers: result.offers || [],
total: result.total || 0,
page: result.page || 1,
limit: result.limit || 20,
};
} }
export async function getMyOffers(sessionToken: string): Promise<P2POffer[]> { export async function getMyOffers(sessionToken: string): Promise<P2POffer[]> {
const result = await callEdgeFunction<{ success: boolean; offers: P2POffer[] }>( const result = await callEdgeFunction<{ success: boolean; offers: P2POffer[] }>('get-my-offers', {
'get-my-offers', sessionToken,
{ sessionToken } });
);
return result.offers || []; return result.offers || [];
} }
@@ -192,7 +194,9 @@ interface AcceptOfferParams {
buyerWallet: string; buyerWallet: string;
} }
export async function acceptP2POffer(params: AcceptOfferParams): Promise<{ tradeId: string; trade: P2PTrade }> { export async function acceptP2POffer(
params: AcceptOfferParams
): Promise<{ tradeId: string; trade: P2PTrade }> {
return callEdgeFunction<{ success: boolean; tradeId: string; trade: P2PTrade }>( return callEdgeFunction<{ success: boolean; tradeId: string; trade: P2PTrade }>(
'accept-p2p-offer', 'accept-p2p-offer',
params as unknown as Record<string, unknown> params as unknown as Record<string, unknown>
@@ -215,7 +219,9 @@ interface CreateOfferParams {
adType?: 'buy' | 'sell'; adType?: 'buy' | 'sell';
} }
export async function createP2POffer(params: CreateOfferParams): Promise<{ offerId: string; offer: P2POffer }> { export async function createP2POffer(
params: CreateOfferParams
): Promise<{ offerId: string; offer: P2POffer }> {
const result = await callEdgeFunction<{ success: boolean; offer_id: string; offer: P2POffer }>( const result = await callEdgeFunction<{ success: boolean; offer_id: string; offer: P2POffer }>(
'create-offer-telegram', 'create-offer-telegram',
params as unknown as Record<string, unknown> params as unknown as Record<string, unknown>
@@ -239,26 +245,37 @@ export async function getP2PTrades(
// ─── Trade Actions ─────────────────────────────────────────── // ─── Trade Actions ───────────────────────────────────────────
export async function markTradePaid(sessionToken: string, tradeId: string): Promise<P2PTrade> { export async function markTradePaid(sessionToken: string, tradeId: string): Promise<P2PTrade> {
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>( const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
'trade-action', sessionToken,
{ sessionToken, tradeId, action: 'mark_paid' } tradeId,
); action: 'mark_paid',
});
return result.trade; return result.trade;
} }
export async function confirmTradePayment(sessionToken: string, tradeId: string): Promise<P2PTrade> { export async function confirmTradePayment(
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>( sessionToken: string,
'trade-action', tradeId: string
{ sessionToken, tradeId, action: 'confirm' } ): Promise<P2PTrade> {
); const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
sessionToken,
tradeId,
action: 'confirm',
});
return result.trade; return result.trade;
} }
export async function cancelTrade(sessionToken: string, tradeId: string, reason?: string): Promise<P2PTrade> { export async function cancelTrade(
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>( sessionToken: string,
'trade-action', tradeId: string,
{ sessionToken, tradeId, action: 'cancel', payload: { reason } } reason?: string
); ): Promise<P2PTrade> {
const result = await callEdgeFunction<{ success: boolean; trade: P2PTrade }>('trade-action', {
sessionToken,
tradeId,
action: 'cancel',
payload: { reason },
});
return result.trade; return result.trade;
} }
@@ -268,10 +285,12 @@ export async function rateTrade(
rating: number, rating: number,
review?: string review?: string
): Promise<void> { ): Promise<void> {
await callEdgeFunction<{ success: boolean }>( await callEdgeFunction<{ success: boolean }>('trade-action', {
'trade-action', sessionToken,
{ sessionToken, tradeId, action: 'rate', payload: { rating, review } } tradeId,
); action: 'rate',
payload: { rating, review },
});
} }
// ─── Messages ──────────────────────────────────────────────── // ─── Messages ────────────────────────────────────────────────
@@ -281,10 +300,12 @@ export async function sendTradeMessage(
tradeId: string, tradeId: string,
message: string message: string
): Promise<string> { ): Promise<string> {
const result = await callEdgeFunction<{ success: boolean; messageId: string }>( const result = await callEdgeFunction<{ success: boolean; messageId: string }>('p2p-messages', {
'p2p-messages', sessionToken,
{ sessionToken, action: 'send', tradeId, message } action: 'send',
); tradeId,
message,
});
return result.messageId; return result.messageId;
} }
@@ -307,10 +328,13 @@ export async function openDispute(
reason: string, reason: string,
category: string category: string
): Promise<P2PDispute> { ): Promise<P2PDispute> {
const result = await callEdgeFunction<{ success: boolean; dispute: P2PDispute }>( const result = await callEdgeFunction<{ success: boolean; dispute: P2PDispute }>('p2p-dispute', {
'p2p-dispute', sessionToken,
{ sessionToken, action: 'open', tradeId, reason, category } action: 'open',
); tradeId,
reason,
category,
});
return result.dispute; return result.dispute;
} }
@@ -321,8 +345,12 @@ export async function addDisputeEvidence(
evidenceType: string, evidenceType: string,
description?: string description?: string
): Promise<void> { ): Promise<void> {
await callEdgeFunction<{ success: boolean }>( await callEdgeFunction<{ success: boolean }>('p2p-dispute', {
'p2p-dispute', sessionToken,
{ sessionToken, action: 'add_evidence', tradeId, evidenceUrl, evidenceType, description } action: 'add_evidence',
); tradeId,
evidenceUrl,
evidenceType,
description,
});
} }
+67 -37
View File
@@ -40,23 +40,33 @@ export function P2PSection() {
const fetchMyTrades = useCallback(async () => { const fetchMyTrades = useCallback(async () => {
if (!sessionToken) return; if (!sessionToken) return;
setMyDataLoading(true);
try { try {
const trades = await getP2PTrades(sessionToken, 'all'); const trades = await getP2PTrades(sessionToken, 'all');
setMyTrades(trades); setMyTrades(trades);
} catch (err) { } catch (err) {
console.error('Failed to fetch my trades:', err); console.error('Failed to fetch my trades:', err);
} finally {
setMyDataLoading(false);
} }
}, [sessionToken]); }, [sessionToken]);
const fetchMyOffersFull = useCallback(async () => {
setMyDataLoading(true);
try {
await fetchMyOffers();
} finally {
setMyDataLoading(false);
}
}, [fetchMyOffers]);
useEffect(() => { useEffect(() => {
if (activeTab === 'myAds') { if (activeTab === 'myAds') {
setMyDataLoading(true); fetchMyOffersFull();
fetchMyOffers().finally(() => setMyDataLoading(false));
} else if (activeTab === 'myTrades') { } else if (activeTab === 'myTrades') {
setMyDataLoading(true); fetchMyTrades();
fetchMyTrades().finally(() => setMyDataLoading(false));
} }
}, [activeTab, fetchMyOffers, fetchMyTrades]); }, [activeTab, fetchMyOffersFull, fetchMyTrades]);
const handleTabChange = (tab: Tab) => { const handleTabChange = (tab: Tab) => {
hapticImpact('light'); hapticImpact('light');
@@ -81,10 +91,7 @@ export function P2PSection() {
if (activeTradeId) { if (activeTradeId) {
return ( return (
<div className="h-full overflow-y-auto p-4 safe-area-top"> <div className="h-full overflow-y-auto p-4 safe-area-top">
<TradeView <TradeView tradeId={activeTradeId} onBack={() => setActiveTradeId(null)} />
tradeId={activeTradeId}
onBack={() => setActiveTradeId(null)}
/>
</div> </div>
); );
} }
@@ -98,13 +105,21 @@ export function P2PSection() {
const statusColor = (s: string) => { const statusColor = (s: string) => {
switch (s) { switch (s) {
case 'open': return 'text-green-400'; case 'open':
case 'pending': return 'text-amber-400'; return 'text-green-400';
case 'payment_sent': return 'text-blue-400'; case 'pending':
case 'completed': return 'text-green-400'; return 'text-amber-400';
case 'cancelled': case 'refunded': return 'text-red-400'; case 'payment_sent':
case 'disputed': return 'text-orange-400'; return 'text-blue-400';
default: return 'text-muted-foreground'; case 'completed':
return 'text-green-400';
case 'cancelled':
case 'refunded':
return 'text-red-400';
case 'disputed':
return 'text-orange-400';
default:
return 'text-muted-foreground';
} }
}; };
@@ -118,7 +133,10 @@ export function P2PSection() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-xl font-bold text-foreground">{t('p2p.title')}</h1> <h1 className="text-xl font-bold text-foreground">{t('p2p.title')}</h1>
<button <button
onClick={() => { hapticImpact('medium'); setShowCreateOffer(true); }} onClick={() => {
hapticImpact('medium');
setShowCreateOffer(true);
}}
className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-sm font-medium rounded-lg transition-colors" className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-sm font-medium rounded-lg transition-colors"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@@ -137,9 +155,7 @@ export function P2PSection() {
onClick={() => handleTabChange(tab.id)} onClick={() => handleTabChange(tab.id)}
className={cn( className={cn(
'flex-1 py-2 text-xs font-medium rounded-lg transition-colors', 'flex-1 py-2 text-xs font-medium rounded-lg transition-colors',
activeTab === tab.id activeTab === tab.id ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground'
? 'bg-card text-foreground shadow-sm'
: 'text-muted-foreground'
)} )}
> >
{tab.label} {tab.label}
@@ -159,8 +175,8 @@ export function P2PSection() {
)} )}
{/* My Ads */} {/* My Ads */}
{activeTab === 'myAds' && ( {activeTab === 'myAds' &&
myDataLoading ? ( (myDataLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 text-primary animate-spin" /> <Loader2 className="w-6 h-6 text-primary animate-spin" />
</div> </div>
@@ -177,13 +193,20 @@ export function P2PSection() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{myOffers.map((offer) => ( {myOffers.map((offer) => (
<div key={offer.id} className="bg-card rounded-xl border border-border p-3 space-y-1"> <div
key={offer.id}
className="bg-card rounded-xl border border-border p-3 space-y-1"
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={cn( <span
'text-xs font-medium px-2 py-0.5 rounded-full', className={cn(
offer.ad_type === 'sell' ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400' 'text-xs font-medium px-2 py-0.5 rounded-full',
)}> offer.ad_type === 'sell'
? 'bg-red-500/10 text-red-400'
: 'bg-green-500/10 text-green-400'
)}
>
{offer.ad_type === 'sell' ? t('p2p.sell') : t('p2p.buy')} {offer.ad_type === 'sell' ? t('p2p.sell') : t('p2p.buy')}
</span> </span>
<span className="text-sm font-bold text-foreground">{offer.token}</span> <span className="text-sm font-bold text-foreground">{offer.token}</span>
@@ -193,18 +216,21 @@ export function P2PSection() {
</span> </span>
</div> </div>
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span>{offer.remaining_amount}/{offer.amount_crypto} {offer.token}</span> <span>
<span>{offer.price_per_unit?.toLocaleString()} {offer.fiat_currency}</span> {offer.remaining_amount}/{offer.amount_crypto} {offer.token}
</span>
<span>
{offer.price_per_unit?.toLocaleString()} {offer.fiat_currency}
</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) ))}
)}
{/* My Trades */} {/* My Trades */}
{activeTab === 'myTrades' && ( {activeTab === 'myTrades' &&
myDataLoading ? ( (myDataLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 text-primary animate-spin" /> <Loader2 className="w-6 h-6 text-primary animate-spin" />
</div> </div>
@@ -218,7 +244,10 @@ export function P2PSection() {
{myTrades.map((trade) => ( {myTrades.map((trade) => (
<button <button
key={trade.id} key={trade.id}
onClick={() => { hapticImpact('light'); setActiveTradeId(trade.id); }} onClick={() => {
hapticImpact('light');
setActiveTradeId(trade.id);
}}
className="w-full text-left bg-card rounded-xl border border-border p-3 space-y-1 hover:border-cyan-500/50 transition-colors" className="w-full text-left bg-card rounded-xl border border-border p-3 space-y-1 hover:border-cyan-500/50 transition-colors"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -230,14 +259,15 @@ export function P2PSection() {
</span> </span>
</div> </div>
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span>{trade.fiat_amount?.toLocaleString()} {trade.fiat_currency || ''}</span> <span>
{trade.fiat_amount?.toLocaleString()} {trade.fiat_currency || ''}
</span>
<span>{new Date(trade.created_at).toLocaleDateString()}</span> <span>{new Date(trade.created_at).toLocaleDateString()}</span>
</div> </div>
</button> </button>
))} ))}
</div> </div>
) ))}
)}
</div> </div>
{/* Trade Accept Modal */} {/* Trade Accept Modal */}
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.0.224", "version": "1.0.225",
"buildTime": "2026-02-25T16:46:10.349Z", "buildTime": "2026-02-26T14:51:09.120Z",
"buildNumber": 1772037970349 "buildNumber": 1772117469121
} }