mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 18:17:58 +00:00
refactor: reorganize docs folder structure and update P2P plan
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import {
|
||||
getPaymentMethods,
|
||||
validatePaymentDetails,
|
||||
@@ -22,13 +23,14 @@ interface CreateAdProps {
|
||||
|
||||
export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
const { user } = useAuth();
|
||||
const { api, selectedAccount } = usePolkadot();
|
||||
const { account } = useWallet();
|
||||
|
||||
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Form fields
|
||||
const [adType, setAdType] = useState<'buy' | 'sell'>('sell');
|
||||
const [token, setToken] = useState<CryptoToken>('HEZ');
|
||||
const [amountCrypto, setAmountCrypto] = useState('');
|
||||
const [fiatCurrency, setFiatCurrency] = useState<FiatCurrency>('TRY');
|
||||
@@ -73,8 +75,11 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
};
|
||||
|
||||
const handleCreateAd = async () => {
|
||||
if (!api || !selectedAccount || !user) {
|
||||
console.log('🔥 handleCreateAd called', { account, user: user?.id });
|
||||
|
||||
if (!account || !user) {
|
||||
toast.error('Please connect your wallet and log in');
|
||||
console.log('❌ No account or user', { account, user });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,25 +127,41 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// const _offerId = await createFiatOffer({
|
||||
// api,
|
||||
// account: selectedAccount,
|
||||
// token,
|
||||
// amountCrypto: cryptoAmt,
|
||||
// fiatCurrency,
|
||||
// fiatAmount: fiatAmt,
|
||||
// paymentMethodId: selectedPaymentMethod.id,
|
||||
// paymentDetails,
|
||||
// timeLimitMinutes: timeLimit,
|
||||
// minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : undefined,
|
||||
// maxOrderAmount: maxOrderAmount ? parseFloat(maxOrderAmount) : undefined
|
||||
// });
|
||||
// Insert offer into Supabase
|
||||
// Note: payment_details_encrypted is stored as JSON string (encryption handled server-side in prod)
|
||||
const { data, error } = await supabase
|
||||
.from('p2p_fiat_offers')
|
||||
.insert({
|
||||
seller_id: user.id,
|
||||
seller_wallet: account,
|
||||
ad_type: adType,
|
||||
token,
|
||||
amount_crypto: cryptoAmt,
|
||||
remaining_amount: cryptoAmt,
|
||||
fiat_currency: fiatCurrency,
|
||||
fiat_amount: fiatAmt,
|
||||
payment_method_id: selectedPaymentMethod.id,
|
||||
payment_details_encrypted: JSON.stringify(paymentDetails),
|
||||
time_limit_minutes: timeLimit,
|
||||
min_order_amount: minOrderAmount ? parseFloat(minOrderAmount) : null,
|
||||
max_order_amount: maxOrderAmount ? parseFloat(maxOrderAmount) : null,
|
||||
status: 'open'
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('❌ Supabase error:', error);
|
||||
toast.error(error.message || 'Failed to create offer');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Offer created successfully:', data);
|
||||
toast.success('Ad created successfully!');
|
||||
onAdCreated();
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Create ad error:', error);
|
||||
// Error toast already shown in createFiatOffer
|
||||
toast.error('Failed to create offer');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -155,6 +176,34 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Ad Type Selection */}
|
||||
<div>
|
||||
<Label>I want to</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={adType === 'sell' ? 'default' : 'outline'}
|
||||
className={adType === 'sell' ? 'bg-red-600 hover:bg-red-700' : ''}
|
||||
onClick={() => setAdType('sell')}
|
||||
>
|
||||
Sell {token}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={adType === 'buy' ? 'default' : 'outline'}
|
||||
className={adType === 'buy' ? 'bg-green-600 hover:bg-green-700' : ''}
|
||||
onClick={() => setAdType('buy')}
|
||||
>
|
||||
Buy {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'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Crypto Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock } from 'lucide-react';
|
||||
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store } from 'lucide-react';
|
||||
import { AdList } from './AdList';
|
||||
import { CreateAd } from './CreateAd';
|
||||
import { NotificationBell } from './NotificationBell';
|
||||
import { QuickFilterBar, DEFAULT_FILTERS, type P2PFilters } from './OrderFilters';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
@@ -20,6 +21,7 @@ interface UserStats {
|
||||
export function P2PDashboard() {
|
||||
const [showCreateAd, setShowCreateAd] = useState(false);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ activeTrades: 0, completedTrades: 0, totalVolume: 0 });
|
||||
const [filters, setFilters] = useState<P2PFilters>(DEFAULT_FILTERS);
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
@@ -78,6 +80,14 @@ export function P2PDashboard() {
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate('/p2p/merchant')}
|
||||
className="border-gray-700 hover:bg-gray-800"
|
||||
>
|
||||
<Store className="w-4 h-4 mr-2" />
|
||||
Merchant
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate('/p2p/orders')}
|
||||
@@ -147,22 +157,27 @@ export function P2PDashboard() {
|
||||
{showCreateAd ? (
|
||||
<CreateAd onAdCreated={() => setShowCreateAd(false)} />
|
||||
) : (
|
||||
<Tabs defaultValue="buy">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="buy">Buy</TabsTrigger>
|
||||
<TabsTrigger value="sell">Sell</TabsTrigger>
|
||||
<TabsTrigger value="my-ads">My Ads</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="buy">
|
||||
<AdList type="buy" />
|
||||
</TabsContent>
|
||||
<TabsContent value="sell">
|
||||
<AdList type="sell" />
|
||||
</TabsContent>
|
||||
<TabsContent value="my-ads">
|
||||
<AdList type="my-ads" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<>
|
||||
{/* Quick Filter Bar */}
|
||||
<QuickFilterBar filters={filters} onFiltersChange={setFilters} />
|
||||
|
||||
<Tabs defaultValue="buy">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="buy">Buy</TabsTrigger>
|
||||
<TabsTrigger value="sell">Sell</TabsTrigger>
|
||||
<TabsTrigger value="my-ads">My Ads</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="buy">
|
||||
<AdList type="buy" filters={filters} />
|
||||
</TabsContent>
|
||||
<TabsContent value="sell">
|
||||
<AdList type="sell" filters={filters} />
|
||||
</TabsContent>
|
||||
<TabsContent value="my-ads">
|
||||
<AdList type="my-ads" filters={filters} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -44,6 +44,12 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent self-trading
|
||||
if (offer.seller_id === user.id) {
|
||||
toast.error('You cannot trade with your own offer');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidAmount) {
|
||||
toast.error('Invalid amount');
|
||||
return;
|
||||
|
||||
@@ -8,10 +8,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from 'sonner';
|
||||
import { MerchantTierBadge } from '@/components/p2p/MerchantTierBadge';
|
||||
import { MerchantApplication } from '@/components/p2p/MerchantApplication';
|
||||
import { CreateAd } from '@/components/p2p/CreateAd';
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -66,6 +68,10 @@ interface ActiveAd {
|
||||
status: string;
|
||||
created_at: string;
|
||||
is_featured: boolean;
|
||||
ad_type: 'buy' | 'sell';
|
||||
min_order_amount?: number;
|
||||
max_order_amount?: number;
|
||||
time_limit_minutes: number;
|
||||
}
|
||||
|
||||
interface MerchantTier {
|
||||
@@ -75,20 +81,11 @@ interface MerchantTier {
|
||||
featured_ads_allowed: number;
|
||||
}
|
||||
|
||||
// Mock data for charts (will be replaced with real data)
|
||||
const generateChartData = () => {
|
||||
const data = [];
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
data.push({
|
||||
date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
volume: Math.floor(Math.random() * 5000) + 1000,
|
||||
trades: Math.floor(Math.random() * 10) + 1
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
interface ChartDataPoint {
|
||||
date: string;
|
||||
volume: number;
|
||||
trades: number;
|
||||
}
|
||||
|
||||
export default function P2PMerchantDashboard() {
|
||||
const navigate = useNavigate();
|
||||
@@ -96,10 +93,19 @@ export default function P2PMerchantDashboard() {
|
||||
const [stats, setStats] = useState<MerchantStats | null>(null);
|
||||
const [tierInfo, setTierInfo] = useState<MerchantTier | null>(null);
|
||||
const [activeAds, setActiveAds] = useState<ActiveAd[]>([]);
|
||||
const [chartData] = useState(generateChartData());
|
||||
const [chartData, setChartData] = useState<ChartDataPoint[]>([]);
|
||||
const [autoReplyOpen, setAutoReplyOpen] = useState(false);
|
||||
const [autoReplyMessage, setAutoReplyMessage] = useState('');
|
||||
const [savingAutoReply, setSavingAutoReply] = useState(false);
|
||||
const [createAdOpen, setCreateAdOpen] = useState(false);
|
||||
|
||||
// Edit ad state
|
||||
const [editAdOpen, setEditAdOpen] = useState(false);
|
||||
const [editingAd, setEditingAd] = useState<ActiveAd | null>(null);
|
||||
const [editFiatAmount, setEditFiatAmount] = useState('');
|
||||
const [editMinOrder, setEditMinOrder] = useState('');
|
||||
const [editMaxOrder, setEditMaxOrder] = useState('');
|
||||
const [savingEdit, setSavingEdit] = useState(false);
|
||||
|
||||
// Fetch merchant data
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -144,6 +150,46 @@ export default function P2PMerchantDashboard() {
|
||||
if (adsData) {
|
||||
setActiveAds(adsData);
|
||||
}
|
||||
|
||||
// Fetch chart data - last 30 days trades
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const { data: tradesData } = await supabase
|
||||
.from('p2p_fiat_trades')
|
||||
.select('created_at, fiat_amount, status')
|
||||
.or(`seller_id.eq.${user.id},buyer_id.eq.${user.id}`)
|
||||
.gte('created_at', thirtyDaysAgo.toISOString())
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
// Group trades by day
|
||||
const tradesByDay: Record<string, { volume: number; trades: number }> = {};
|
||||
|
||||
// Initialize all 30 days with 0
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateKey = date.toISOString().split('T')[0];
|
||||
tradesByDay[dateKey] = { volume: 0, trades: 0 };
|
||||
}
|
||||
|
||||
// Fill in actual data
|
||||
tradesData?.forEach((trade) => {
|
||||
const dateKey = new Date(trade.created_at).toISOString().split('T')[0];
|
||||
if (tradesByDay[dateKey]) {
|
||||
tradesByDay[dateKey].volume += trade.fiat_amount || 0;
|
||||
tradesByDay[dateKey].trades += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to array for chart
|
||||
const chartDataArray: ChartDataPoint[] = Object.entries(tradesByDay).map(([date, data]) => ({
|
||||
date: new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
volume: data.volume,
|
||||
trades: data.trades
|
||||
}));
|
||||
|
||||
setChartData(chartDataArray);
|
||||
} catch (error) {
|
||||
console.error('Error fetching merchant data:', error);
|
||||
} finally {
|
||||
@@ -194,6 +240,52 @@ export default function P2PMerchantDashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
// Open edit modal with ad data
|
||||
const openEditModal = (ad: ActiveAd) => {
|
||||
setEditingAd(ad);
|
||||
setEditFiatAmount(ad.fiat_amount.toString());
|
||||
setEditMinOrder(ad.min_order_amount?.toString() || '');
|
||||
setEditMaxOrder(ad.max_order_amount?.toString() || '');
|
||||
setEditAdOpen(true);
|
||||
};
|
||||
|
||||
// Save ad edits
|
||||
const saveAdEdit = async () => {
|
||||
if (!editingAd) return;
|
||||
|
||||
const fiatAmt = parseFloat(editFiatAmount);
|
||||
if (!fiatAmt || fiatAmt <= 0) {
|
||||
toast.error('Invalid fiat amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingEdit(true);
|
||||
try {
|
||||
const updateData: Record<string, unknown> = {
|
||||
fiat_amount: fiatAmt,
|
||||
min_order_amount: editMinOrder ? parseFloat(editMinOrder) : null,
|
||||
max_order_amount: editMaxOrder ? parseFloat(editMaxOrder) : null,
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from('p2p_fiat_offers')
|
||||
.update(updateData)
|
||||
.eq('id', editingAd.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Ad updated successfully');
|
||||
setEditAdOpen(false);
|
||||
setEditingAd(null);
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error updating ad:', error);
|
||||
toast.error('Failed to update ad');
|
||||
} finally {
|
||||
setSavingEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Save auto-reply message
|
||||
const saveAutoReply = async () => {
|
||||
setSavingAutoReply(true);
|
||||
@@ -423,7 +515,7 @@ export default function P2PMerchantDashboard() {
|
||||
</h2>
|
||||
<Button
|
||||
className="bg-kurdish-green hover:bg-kurdish-green-dark"
|
||||
onClick={() => navigate('/p2p/create-ad')}
|
||||
onClick={() => setCreateAdOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Create New Ad
|
||||
@@ -437,7 +529,7 @@ export default function P2PMerchantDashboard() {
|
||||
<p className="text-muted-foreground mb-4">
|
||||
You don't have any active ads yet.
|
||||
</p>
|
||||
<Button onClick={() => navigate('/p2p/create-ad')}>
|
||||
<Button onClick={() => setCreateAdOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Create Your First Ad
|
||||
</Button>
|
||||
@@ -496,7 +588,7 @@ export default function P2PMerchantDashboard() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(`/p2p/edit-ad/${ad.id}`)}
|
||||
onClick={() => openEditModal(ad)}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
@@ -655,6 +747,108 @@ export default function P2PMerchantDashboard() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Ad Modal */}
|
||||
<Dialog open={createAdOpen} onOpenChange={setCreateAdOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New P2P Offer</DialogTitle>
|
||||
<DialogDescription>
|
||||
Lock your crypto in escrow and set your price
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CreateAd onAdCreated={() => {
|
||||
setCreateAdOpen(false);
|
||||
fetchData();
|
||||
}} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Ad Modal */}
|
||||
<Dialog open={editAdOpen} onOpenChange={setEditAdOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Ad</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your ad price and order limits
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editingAd && (
|
||||
<div className="space-y-4">
|
||||
{/* Ad Info (Read-only) */}
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-muted-foreground">Ad Type</span>
|
||||
<Badge variant={editingAd.ad_type === 'sell' ? 'default' : 'secondary'}>
|
||||
{editingAd.ad_type === 'sell' ? 'Selling' : 'Buying'} {editingAd.token}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Amount</span>
|
||||
<span className="font-medium">{editingAd.amount_crypto} {editingAd.token}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fiat Amount */}
|
||||
<div>
|
||||
<Label htmlFor="editFiatAmount">Total Fiat Amount ({editingAd.fiat_currency})</Label>
|
||||
<Input
|
||||
id="editFiatAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={editFiatAmount}
|
||||
onChange={(e) => setEditFiatAmount(e.target.value)}
|
||||
placeholder="Enter fiat amount"
|
||||
/>
|
||||
{editFiatAmount && editingAd.amount_crypto > 0 && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Price per {editingAd.token}: {(parseFloat(editFiatAmount) / editingAd.amount_crypto).toFixed(2)} {editingAd.fiat_currency}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Order Limits */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="editMinOrder">Min Order ({editingAd.token})</Label>
|
||||
<Input
|
||||
id="editMinOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={editMinOrder}
|
||||
onChange={(e) => setEditMinOrder(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="editMaxOrder">Max Order ({editingAd.token})</Label>
|
||||
<Input
|
||||
id="editMaxOrder"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={editMaxOrder}
|
||||
onChange={(e) => setEditMaxOrder(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditAdOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-kurdish-green hover:bg-kurdish-green-dark"
|
||||
onClick={saveAdEdit}
|
||||
disabled={savingEdit}
|
||||
>
|
||||
{savingEdit && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user