Files
pwap/web/src/pages/launchpad/PresaleDetail.tsx
T
pezkuwichain 8d30519efc Fix all shadow deprecation warnings across entire mobile app
- Replaced shadowColor/shadowOffset/shadowOpacity/shadowRadius with boxShadow
- Fixed 28 files (21 screens + 7 components)
- Preserved elevation for Android compatibility
- All React Native Web deprecation warnings resolved

Files fixed:
- All screen components
- All reusable components
- Navigation components
- Modal components
2026-01-14 15:05:10 +03:00

540 lines
21 KiB
TypeScript

import { useState, useEffect } from 'react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { useParams, useNavigate } from 'react-router-dom';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Loader2,
ArrowLeft,
Users,
Clock,
Target,
AlertCircle,
CheckCircle2,
Wallet,
RefreshCcw,
} from 'lucide-react';
import { toast } from 'sonner';
interface PresaleData {
owner: string;
paymentAsset: number;
rewardAsset: number;
tokensForSale: string;
startBlock: number;
endBlock: number;
status: { Active?: null; Finalized?: null; Cancelled?: null };
isWhitelist: boolean;
minContribution: string;
maxContribution: string;
hardCap: string;
}
export default function PresaleDetail() {
const { id } = useParams();
const { api, selectedAccount, isApiReady } = usePezkuwi();
const { balances } = useWallet();
const navigate = useNavigate();
const [presale, setPresale] = useState<PresaleData | null>(null);
const [loading, setLoading] = useState(true);
const [contributing, setContributing] = useState(false);
const [refunding, setRefunding] = useState(false);
const [amount, setAmount] = useState('');
const [myContribution, setMyContribution] = useState('0');
const [currentBlock, setCurrentBlock] = useState(0);
const [totalRaised, setTotalRaised] = useState('0');
const [contributorsCount, setContributorsCount] = useState(0);
const loadPresaleData = async () => {
if (!api || !id) return;
try {
const header = await api.rpc.chain.getHeader();
setCurrentBlock(header.number.toNumber());
const presaleData = await api.query.presale.presales(parseInt(id));
if (presaleData.isSome) {
const data = presaleData.unwrap().toJSON() as PresaleData;
setPresale(data);
const raised = await api.query.presale.totalRaised(parseInt(id));
setTotalRaised((raised.toString() / 1_000_000).toFixed(2));
const contributors = await api.query.presale.contributors(parseInt(id));
if (contributors.isSome) {
const contributorsList = contributors.unwrap();
setContributorsCount(contributorsList.length);
}
if (selectedAccount) {
const contribution = await api.query.presale.contributions(
parseInt(id),
selectedAccount.address
);
if (contribution.isSome) {
const contrib = contribution.unwrap();
setMyContribution((contrib.amount.toString() / 1_000_000).toFixed(2));
}
}
}
setLoading(false);
} catch (error) {
console.error('Load presale error:', error);
setLoading(false);
}
};
useEffect(() => {
if (isApiReady && id) {
loadPresaleData();
const interval = setInterval(loadPresaleData, 10000);
return () => clearInterval(interval);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [api, selectedAccount, isApiReady, id]);
const handleContribute = async () => {
if (!api || !selectedAccount || !amount || !id) return;
const amountValue = parseFloat(amount);
if (isNaN(amountValue) || amountValue <= 0) {
toast.error('Please enter a valid amount');
return;
}
setContributing(true);
try {
const amountInSmallestUnit = Math.floor(amountValue * 1_000_000);
const tx = api.tx.presale.contribute(parseInt(id), amountInSmallestUnit);
await tx.signAndSend(selectedAccount.address, ({ status, events }) => {
if (status.isInBlock) {
toast.success('Contribution submitted!');
}
if (status.isFinalized) {
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicSuccess.is(event)) {
toast.success('Contribution successful!');
setAmount('');
loadPresaleData();
} else if (api.events.system.ExtrinsicFailed.is(event)) {
toast.error('Contribution failed');
}
});
setContributing(false);
}
});
} catch (error) {
console.error('Contribution error:', error);
toast.error((error as Error).message || 'Failed to contribute');
setContributing(false);
}
};
const handleRefund = async () => {
if (!api || !selectedAccount || !id) return;
setRefunding(true);
try {
const tx = api.tx.presale.refund(parseInt(id));
await tx.signAndSend(selectedAccount.address, ({ status, events }) => {
if (status.isInBlock) {
toast.success('Refund submitted!');
}
if (status.isFinalized) {
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicSuccess.is(event)) {
toast.success('Refund successful!');
loadPresaleData();
} else if (api.events.system.ExtrinsicFailed.is(event)) {
toast.error('Refund failed');
}
});
setRefunding(false);
}
});
} catch (error) {
console.error('Refund error:', error);
toast.error((error as Error).message || 'Failed to refund');
setRefunding(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (!presale) {
return null;
}
const getTimeRemaining = () => {
const endBlock = parseInt(presale.startBlock) + parseInt(presale.duration);
const remaining = endBlock - currentBlock;
if (remaining <= 0) return 'Ended';
const days = Math.floor((remaining * 6) / 86400);
const hours = Math.floor(((remaining * 6) % 86400) / 3600);
const minutes = Math.floor(((remaining * 6) % 3600) / 60);
return `${days}d ${hours}h ${minutes}m`;
};
const getProgress = () => {
const raised = parseFloat(totalRaised) / 1_000_000;
const cap = parseFloat(presale.limits.hardCap.replace(/,/g, '')) / 1_000_000;
return (raised / cap) * 100;
};
const calculateReward = () => {
const amountValue = parseFloat(amount);
if (isNaN(amountValue)) return 0;
// Dynamic calculation: (user_contribution / total_raised) * tokens_for_sale
const raised = parseFloat(totalRaised) || 1; // Avoid division by zero
const tokensForSale = parseFloat(presale.tokensForSale.replace(/,/g, ''));
return (amountValue / raised) * tokensForSale;
};
const getCurrentRate = () => {
if (!presale || !totalRaised) return 'Dynamic';
const raised = parseFloat(totalRaised) / 1_000_000;
if (raised === 0) return 'TBD';
const tokensForSale = parseFloat(presale.tokensForSale.replace(/,/g, '')) / 1_000_000;
return `1:${(tokensForSale / raised).toFixed(2)}`;
};
const isGracePeriod = () => {
const graceEnd = parseInt(presale.startBlock) + parseInt(presale.gracePeriodBlocks);
return currentBlock <= graceEnd;
};
const wusdtBalance = balances.find((b) => b.assetId === 1000)?.balance || '0';
return (
<div className="container mx-auto px-4 py-8 max-w-6xl">
{/* Back Button */}
<Button
variant="ghost"
onClick={() => navigate('/launchpad')}
className="mb-6 gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back to Launchpad
</Button>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Header */}
<Card className="p-6">
<div className="flex justify-between items-start mb-4">
<div>
<h1 className="text-3xl font-bold mb-2">Presale #{id}</h1>
<p className="text-muted-foreground">
Current Rate: {getCurrentRate()} (Dynamic)
</p>
</div>
<div className="flex gap-2">
<Badge
variant={
presale.status.type === 'Active'
? 'default'
: presale.status.type === 'Finalized'
? 'secondary'
: 'destructive'
}
>
{presale.status.type}
</Badge>
<Badge variant="outline">{presale.accessControl.type}</Badge>
</div>
</div>
{/* Progress */}
<div className="mb-6">
<div className="flex justify-between text-sm mb-2">
<span className="font-semibold">Progress</span>
<span className="font-semibold">{getProgress().toFixed(2)}%</span>
</div>
<Progress value={getProgress()} className="mb-2" />
<div className="flex justify-between text-sm text-muted-foreground">
<span>{(parseFloat(totalRaised) / 1_000_000).toFixed(2)} USDT Raised</span>
<span>
{(parseFloat(presale.limits.hardCap.replace(/,/g, '')) / 1_000_000).toFixed(0)} USDT Hard Cap
</span>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-muted rounded-lg">
<Users className="w-6 h-6 mx-auto mb-2 text-primary" />
<p className="text-2xl font-bold">{contributorsCount}</p>
<p className="text-xs text-muted-foreground">Contributors</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Clock className="w-6 h-6 mx-auto mb-2 text-orange-500" />
<p className="text-lg font-bold">{getTimeRemaining()}</p>
<p className="text-xs text-muted-foreground">Time Left</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Target className="w-6 h-6 mx-auto mb-2 text-green-500" />
<p className="text-lg font-bold">
{(parseFloat(presale.limits.minContribution.replace(/,/g, '')) / 1_000_000).toFixed(0)} USDT
</p>
<p className="text-xs text-muted-foreground">Min</p>
</div>
<div className="text-center p-4 bg-muted rounded-lg">
<Target className="w-6 h-6 mx-auto mb-2 text-blue-500" />
<p className="text-lg font-bold">
{(parseFloat(presale.limits.maxContribution.replace(/,/g, '')) / 1_000_000).toFixed(0)} USDT
</p>
<p className="text-xs text-muted-foreground">Max</p>
</div>
</div>
</Card>
{/* Tabs */}
<Tabs defaultValue="details">
<TabsList className="w-full">
<TabsTrigger value="details" className="flex-1">Details</TabsTrigger>
<TabsTrigger value="vesting" className="flex-1">Vesting</TabsTrigger>
<TabsTrigger value="refund" className="flex-1">Refund Policy</TabsTrigger>
</TabsList>
<TabsContent value="details">
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Presale Information</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Payment Asset</span>
<span className="font-semibold">Asset #{presale.paymentAsset}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Reward Asset</span>
<span className="font-semibold">Asset #{presale.rewardAsset}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tokens For Sale</span>
<span className="font-semibold">
{(parseFloat(presale.tokensForSale.replace(/,/g, '')) / 1_000_000).toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Current Rate</span>
<span className="font-semibold">
{getCurrentRate()}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Duration</span>
<span className="font-semibold">
{Math.floor((parseInt(presale.duration) * 6) / 86400)} days
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Owner</span>
<span className="font-mono text-xs">{presale.owner.slice(0, 12)}...</span>
</div>
</div>
</Card>
</TabsContent>
<TabsContent value="vesting">
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Vesting Schedule</h3>
{presale.vesting ? (
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Immediate Release</span>
<span className="font-semibold">
{presale.vesting.immediateReleasePercent}%
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Vesting Duration</span>
<span className="font-semibold">
{Math.floor((parseInt(presale.vesting.vestingDurationBlocks) * 6) / 86400)} days
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Cliff Period</span>
<span className="font-semibold">
{Math.floor((parseInt(presale.vesting.cliffBlocks) * 6) / 86400)} days
</span>
</div>
</div>
) : (
<p className="text-muted-foreground">No vesting schedule - tokens released immediately</p>
)}
</Card>
</TabsContent>
<TabsContent value="refund">
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Refund Policy</h3>
<div className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{isGracePeriod() ? (
<span className="text-green-600 font-semibold">
Grace Period Active: {presale.graceRefundFeePercent}% fee
</span>
) : (
<span>
Normal Period: {presale.refundFeePercent}% fee
</span>
)}
</AlertDescription>
</Alert>
<div className="space-y-2 text-sm">
<p>
Grace Period ({Math.floor((parseInt(presale.gracePeriodBlocks) * 6) / 3600)}h):
<span className="font-semibold ml-2">{presale.graceRefundFeePercent}% fee</span>
</p>
<p>
After Grace Period:
<span className="font-semibold ml-2">{presale.refundFeePercent}% fee</span>
</p>
<p className="text-muted-foreground text-xs mt-4">
Platform fee split: 50% Treasury, 25% Burn, 25% Stakers
</p>
</div>
</div>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Contribute Card */}
{presale.status.type === 'Active' && (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Contribute</h3>
{!selectedAccount ? (
<Alert>
<Wallet className="h-4 w-4" />
<AlertDescription>
Please connect your wallet to contribute
</AlertDescription>
</Alert>
) : (
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-2 block">Amount (wUSDT)</label>
<Input
type="number"
placeholder="0.00"
value={amount}
onChange={(e) => setAmount(e.target.value)}
disabled={contributing}
/>
<p className="text-xs text-muted-foreground mt-1">
Balance: {(parseFloat(wusdtBalance) / 1_000_000).toFixed(2)} wUSDT
</p>
</div>
{amount && (
<Alert>
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>
You will receive: <strong>{calculateReward().toFixed(2)}</strong> tokens
</AlertDescription>
</Alert>
)}
<Button
className="w-full"
onClick={handleContribute}
disabled={contributing || !amount}
>
{contributing && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
{contributing ? 'Contributing...' : 'Contribute Now'}
</Button>
<div className="text-xs text-center text-muted-foreground">
Platform fee: 2% (split 50-25-25)
</div>
</div>
)}
</Card>
)}
{/* My Contribution Card */}
{selectedAccount && parseFloat(myContribution) > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">My Contribution</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Contributed</span>
<span className="font-bold">
{(parseFloat(myContribution) / 1_000_000).toFixed(2)} wUSDT
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Will Receive (Est.)</span>
<span className="font-bold">
{(() => {
const raised = parseFloat(totalRaised) / 1_000_000;
const myContr = parseFloat(myContribution) / 1_000_000;
const tokensForSale = parseFloat(presale.tokensForSale.replace(/,/g, '')) / 1_000_000;
return raised > 0 ? ((myContr / raised) * tokensForSale).toFixed(2) : '0.00';
})()}{' '}
tokens
</span>
</div>
{presale.status.type === 'Active' && (
<Button
variant="outline"
className="w-full mt-4 gap-2"
onClick={handleRefund}
disabled={refunding}
>
{refunding && <Loader2 className="w-4 h-4 animate-spin" />}
<RefreshCcw className="w-4 h-4" />
{refunding ? 'Processing...' : 'Request Refund'}
</Button>
)}
</div>
</Card>
)}
{/* Info Card */}
<Card className="p-6 bg-muted">
<h4 className="font-semibold mb-3">Important Information</h4>
<ul className="space-y-2 text-sm text-muted-foreground">
<li> Contributions are subject to 2% platform fee</li>
<li> Refunds available before presale ends (fees apply)</li>
<li> Lower fees during grace period</li>
<li> Check vesting schedule before contributing</li>
</ul>
</Card>
</div>
</div>
</div>
);
}